CLAUDE: Add toggleable stats to /player command and injury system improvements

Add interactive PlayerStatsView with toggle buttons to show/hide batting and
pitching statistics independently in the /player command. Stats are hidden by
default with clean, user-friendly buttons (💥 batting,  pitching) that update
the embed in-place. Only the command caller can toggle stats, and buttons
timeout after 5 minutes.

Player Stats Toggle Feature:
- Add views/players.py with PlayerStatsView class
- Update /player command to use interactive view
- Stats hidden by default, shown on button click
- Independent batting/pitching toggles
- User-restricted interactions with timeout handling

Injury System Enhancements:
- Add BatterInjuryModal and PitcherRestModal for injury logging
- Add player_id extraction validator to Injury model
- Fix injury creation to merge API request/response data
- Add responders parameter to BaseView for multi-user interactions

API Client Improvements:
- Handle None values correctly in PATCH query parameters
- Convert None to empty string for nullable fields in database

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Cal Corum 2025-10-17 23:26:08 -05:00
parent 9fca0bc279
commit 82abf3d9e6
7 changed files with 811 additions and 184 deletions

View File

@ -339,7 +339,10 @@ class APIClient:
# Add data as query parameters if requested
if use_query_params and data:
params = [(k, str(v)) for k, v in data.items()]
# Handle None values by converting to empty string
# The database API's PATCH endpoint treats empty strings as NULL for nullable fields
# Example: {'il_return': None} → ?il_return= → Database sets il_return to NULL
params = [(k, '' if v is None else str(v)) for k, v in data.items()]
url = self._add_params(url, params)
await self._ensure_session()

View File

@ -14,8 +14,7 @@ from services.player_service import player_service
from services.stats_service import stats_service
from utils.logging import get_contextual_logger
from utils.decorators import logged_command
from views.embeds import EmbedColors, EmbedTemplate
from models.team import RosterType
from views.players import PlayerStatsView
async def player_name_autocomplete(
@ -155,176 +154,21 @@ class PlayerInfoCommands(commands.Cog):
batting_stats=bool(batting_stats),
pitching_stats=bool(pitching_stats))
# Create comprehensive player embed with statistics
self.logger.debug("Creating Discord embed with statistics")
embed = await self._create_player_embed_with_stats(
player_with_team,
search_season,
batting_stats,
pitching_stats
)
await interaction.followup.send(embed=embed)
async def _create_player_embed_with_stats(
self,
player,
season: int,
batting_stats=None,
pitching_stats=None
) -> discord.Embed:
"""Create a comprehensive player embed with statistics."""
# Determine embed color based on team
embed_color = EmbedColors.PRIMARY
if hasattr(player, 'team') and player.team and hasattr(player.team, 'color'):
try:
# Convert hex color string to int
embed_color = int(player.team.color, 16)
except (ValueError, TypeError):
embed_color = EmbedColors.PRIMARY
# Create base embed
embed = EmbedTemplate.create_base_embed(
title=f"🏟️ {player.name}",
color=embed_color
)
# Set team logo beside player name (as author icon)
if hasattr(player, 'team') and player.team and hasattr(player.team, 'thumbnail') and player.team.thumbnail:
embed.set_author(
name=player.name,
icon_url=player.team.thumbnail
)
# Remove the emoji from title since we're using author
embed.title = None
# Basic info section
embed.add_field(
name="Position",
value=player.primary_position,
inline=True
)
if hasattr(player, 'team') and player.team:
embed.add_field(
name="Team",
value=f"{player.team.abbrev} - {player.team.sname}",
inline=True
)
# Add Major League affiliate if this is a Minor League team
if player.team.roster_type() == RosterType.MINOR_LEAGUE:
major_affiliate = player.team.get_major_league_affiliate()
if major_affiliate:
embed.add_field(
name="Major Affiliate",
value=major_affiliate,
inline=True
)
embed.add_field(
name="sWAR",
value=f"{player.wara:.1f}",
inline=True
# Create interactive player view with toggleable statistics
self.logger.debug("Creating PlayerStatsView with toggleable statistics")
view = PlayerStatsView(
player=player_with_team,
season=search_season,
batting_stats=batting_stats,
pitching_stats=pitching_stats,
user_id=interaction.user.id
)
embed.add_field(
name="Player ID",
value=str(player.id),
inline=True
)
# All positions if multiple
if len(player.positions) > 1:
embed.add_field(
name="Positions",
value=", ".join(player.positions),
inline=True
)
embed.add_field(
name="Season",
value=str(season),
inline=True
)
# Get initial embed with stats hidden
embed = await view.get_initial_embed()
# Add injury rating if available
if player.injury_rating:
embed.add_field(
name="Injury Rating",
value=player.injury_rating,
inline=True
)
# Add batting stats if available
if batting_stats:
self.logger.debug("Adding batting statistics to embed")
batting_value = (
f"**AVG/OBP/SLG:** {batting_stats.avg:.3f}/{batting_stats.obp:.3f}/{batting_stats.slg:.3f}\n"
f"**OPS:** {batting_stats.ops:.3f} | **wOBA:** {batting_stats.woba:.3f}\n"
f"**HR:** {batting_stats.homerun} | **RBI:** {batting_stats.rbi} | **R:** {batting_stats.run}\n"
f"**AB:** {batting_stats.ab} | **H:** {batting_stats.hit} | **BB:** {batting_stats.bb} | **SO:** {batting_stats.so}"
)
embed.add_field(
name="⚾ Batting Stats",
value=batting_value,
inline=False
)
# Add pitching stats if available
if pitching_stats:
self.logger.debug("Adding pitching statistics to embed")
ip = pitching_stats.innings_pitched
pitching_value = (
f"**W-L:** {pitching_stats.win}-{pitching_stats.loss} | **ERA:** {pitching_stats.era:.2f}\n"
f"**WHIP:** {pitching_stats.whip:.2f} | **IP:** {ip:.1f}\n"
f"**SO:** {pitching_stats.so} | **BB:** {pitching_stats.bb} | **H:** {pitching_stats.hits}\n"
f"**GS:** {pitching_stats.gs} | **SV:** {pitching_stats.saves} | **HLD:** {pitching_stats.hold}"
)
embed.add_field(
name="🥎 Pitching Stats",
value=pitching_value,
inline=False
)
# Add a note if no stats are available
if not batting_stats and not pitching_stats:
embed.add_field(
name="📊 Statistics",
value="No statistics available for this season.",
inline=False
)
# Set player card as main image
if player.image:
embed.set_image(url=player.image)
self.logger.debug("Player card image added to embed", image_url=player.image)
# Set thumbnail with priority: fancycard → headshot → team logo
thumbnail_url = None
thumbnail_source = None
if hasattr(player, 'vanity_card') and player.vanity_card:
thumbnail_url = player.vanity_card
thumbnail_source = "fancycard"
elif hasattr(player, 'headshot') and player.headshot:
thumbnail_url = player.headshot
thumbnail_source = "headshot"
elif hasattr(player, 'team') and player.team and hasattr(player.team, 'thumbnail') and player.team.thumbnail:
thumbnail_url = player.team.thumbnail
thumbnail_source = "team logo"
if thumbnail_url:
embed.set_thumbnail(url=thumbnail_url)
self.logger.debug(f"Thumbnail set from {thumbnail_source}", thumbnail_url=thumbnail_url)
# Footer with player ID and additional info
footer_text = f"Player ID: {player.id}"
if batting_stats and pitching_stats:
footer_text += " • Two-way player"
embed.set_footer(text=footer_text)
return embed
# Send with interactive view
await interaction.followup.send(embed=embed, view=view)
async def setup(bot: commands.Bot):

View File

@ -3,8 +3,8 @@ Injury model for tracking player injuries
Represents an injury record with game timeline and status information.
"""
from typing import Optional
from pydantic import Field
from typing import Optional, Any, Dict
from pydantic import Field, model_validator
from models.base import SBABaseModel
@ -19,6 +19,26 @@ class Injury(SBABaseModel):
player_id: int = Field(..., description="Player ID who is injured")
total_games: int = Field(..., description="Total games player will be out")
@model_validator(mode='before')
@classmethod
def extract_player_id(cls, data: Any) -> Any:
"""
Extract player_id from nested player object if present.
The API returns injuries with a nested 'player' object:
{'id': 123, 'player': {'id': 456, ...}, ...}
This validator extracts the player ID before validation:
{'id': 123, 'player_id': 456, ...}
"""
if isinstance(data, dict):
# If player_id is missing but player object exists, extract it
if 'player_id' not in data and 'player' in data:
if isinstance(data['player'], dict) and 'id' in data['player']:
data['player_id'] = data['player']['id']
return data
# Injury timeline
start_week: int = Field(..., description="Week injury started")
start_game: int = Field(..., description="Game number injury started (1-4)")

View File

@ -155,13 +155,22 @@ class InjuryService(BaseService[Injury]):
'is_active': True
}
injury = await self.create(injury_data)
if injury:
logger.info(f"Created injury for player {player_id}: {total_games} games")
return injury
# Call the API to create the injury
client = await self.get_client()
response = await client.post(self.endpoint, injury_data)
logger.error(f"Failed to create injury for player {player_id}")
return None
if not response:
logger.error(f"Failed to create injury for player {player_id}: No response from API")
return None
# Merge the request data with the response to ensure all required fields are present
# (API may not return all fields that were sent)
merged_data = {**injury_data, **response}
# Create Injury model from merged data
injury = Injury.from_api_data(merged_data)
logger.info(f"Created injury for player {player_id}: {total_games} games")
return injury
except Exception as e:
logger.error(f"Error creating injury for player {player_id}: {e}")

View File

@ -4,7 +4,7 @@ Base View Classes for Discord Bot v2.0
Provides foundational view components with consistent styling and behavior.
"""
import logging
from typing import Optional, Any, Callable, Awaitable
from typing import List, Optional, Any, Callable, Awaitable, Union
from datetime import datetime, timezone
import discord
@ -21,20 +21,22 @@ class BaseView(discord.ui.View):
*,
timeout: float = 180.0,
user_id: Optional[int] = None,
responders: Optional[List[int | None]] = None,
logger_name: Optional[str] = None
):
super().__init__(timeout=timeout)
self.user_id = user_id
self.responders = responders
self.logger = get_contextual_logger(logger_name or f'{__name__}.BaseView')
self.interaction_count = 0
self.created_at = datetime.now(timezone.utc)
async def interaction_check(self, interaction: discord.Interaction) -> bool:
"""Check if user is authorized to interact with this view."""
if self.user_id is None:
if self.user_id is None and self.responders is None:
return True
if interaction.user.id != self.user_id:
if (self.user_id is not None and interaction.user.id != self.user_id) or (self.responders is not None and interaction.user.id not in self.responders):
await interaction.response.send_message(
"❌ You cannot interact with this menu.",
ephemeral=True
@ -95,14 +97,15 @@ class ConfirmationView(BaseView):
def __init__(
self,
*,
user_id: int,
user_id: Optional[int] = None,
responders: Optional[List[int | None]] = None,
timeout: float = 60.0,
confirm_callback: Optional[Callable[[discord.Interaction], Awaitable[None]]] = None,
cancel_callback: Optional[Callable[[discord.Interaction], Awaitable[None]]] = None,
confirm_label: str = "Confirm",
cancel_label: str = "Cancel"
):
super().__init__(timeout=timeout, user_id=user_id, logger_name=f'{__name__}.ConfirmationView')
super().__init__(timeout=timeout, user_id=user_id, responders=responders, logger_name=f'{__name__}.ConfirmationView')
self.confirm_callback = confirm_callback
self.cancel_callback = cancel_callback
self.result: Optional[bool] = None

View File

@ -485,4 +485,357 @@ def validate_season(season: str) -> bool:
season_num = int(season)
return 1 <= season_num <= 50
except ValueError:
return False
return False
class BatterInjuryModal(BaseModal):
"""Modal for collecting current week/game when logging batter injury."""
def __init__(
self,
player: 'Player',
injury_games: int,
season: int,
*,
timeout: Optional[float] = 300.0
):
"""
Initialize batter injury modal.
Args:
player: Player object for the injured batter
injury_games: Injury games from roll
season: Current season number
timeout: Modal timeout in seconds
"""
super().__init__(title=f"Batter Injury - {player.name}", timeout=timeout)
self.player = player
self.injury_games = injury_games
self.season = season
# Current week input
self.current_week = discord.ui.TextInput(
label="Current Week",
placeholder="Enter current week number (e.g., 5)",
required=True,
max_length=2,
style=discord.TextStyle.short
)
# Current game input
self.current_game = discord.ui.TextInput(
label="Current Game",
placeholder="Enter current game number (1-4)",
required=True,
max_length=1,
style=discord.TextStyle.short
)
self.add_item(self.current_week)
self.add_item(self.current_game)
async def on_submit(self, interaction: discord.Interaction):
"""Handle batter injury input and log injury."""
from services.player_service import player_service
from services.injury_service import injury_service
import math
# Validate current week
try:
week = int(self.current_week.value)
if week < 1 or week > 18:
raise ValueError("Week must be between 1 and 18")
except ValueError:
embed = EmbedTemplate.error(
title="Invalid Week",
description="Current week must be a number between 1 and 18."
)
await interaction.response.send_message(embed=embed, ephemeral=True)
return
# Validate current game
try:
game = int(self.current_game.value)
if game < 1 or game > 4:
raise ValueError("Game must be between 1 and 4")
except ValueError:
embed = EmbedTemplate.error(
title="Invalid Game",
description="Current game must be a number between 1 and 4."
)
await interaction.response.send_message(embed=embed, ephemeral=True)
return
# Calculate injury dates
out_weeks = math.floor(self.injury_games / 4)
out_games = self.injury_games % 4
return_week = week + out_weeks
return_game = game + 1 + out_games
if return_game > 4:
return_week += 1
return_game -= 4
# Adjust start date if injury starts after game 4
start_week = week if game != 4 else week + 1
start_game = game + 1 if game != 4 else 1
return_date = f'w{return_week:02d}g{return_game}'
# Create injury record
try:
injury = await injury_service.create_injury(
season=self.season,
player_id=self.player.id,
total_games=self.injury_games,
start_week=start_week,
start_game=start_game,
end_week=return_week,
end_game=return_game
)
if not injury:
raise ValueError("Failed to create injury record")
# Update player's il_return field
await player_service.update_player(self.player.id, {'il_return': return_date})
# Success response
embed = EmbedTemplate.success(
title="Injury Logged",
description=f"{self.player.name}'s injury has been logged."
)
embed.add_field(
name="Duration",
value=f"{self.injury_games} game{'s' if self.injury_games > 1 else ''}",
inline=True
)
embed.add_field(
name="Return Date",
value=return_date,
inline=True
)
if self.player.team:
embed.add_field(
name="Team",
value=f"{self.player.team.lname} ({self.player.team.abbrev})",
inline=False
)
self.is_submitted = True
self.result = {
'injury_id': injury.id,
'total_games': self.injury_games,
'return_date': return_date
}
await interaction.response.send_message(embed=embed)
except Exception as e:
self.logger.error("Failed to create batter injury", error=e, player_id=self.player.id)
embed = EmbedTemplate.error(
title="Error",
description="Failed to log the injury. Please try again or contact an administrator."
)
await interaction.response.send_message(embed=embed, ephemeral=True)
class PitcherRestModal(BaseModal):
"""Modal for collecting pitcher rest games when logging injury."""
def __init__(
self,
player: 'Player',
injury_games: int,
season: int,
*,
timeout: Optional[float] = 300.0
):
"""
Initialize pitcher rest modal.
Args:
player: Player object for the injured pitcher
injury_games: Base injury games from roll
season: Current season number
timeout: Modal timeout in seconds
"""
super().__init__(title=f"Pitcher Rest - {player.name}", timeout=timeout)
self.player = player
self.injury_games = injury_games
self.season = season
# Current week input
self.current_week = discord.ui.TextInput(
label="Current Week",
placeholder="Enter current week number (e.g., 5)",
required=True,
max_length=2,
style=discord.TextStyle.short
)
# Current game input
self.current_game = discord.ui.TextInput(
label="Current Game",
placeholder="Enter current game number (1-4)",
required=True,
max_length=1,
style=discord.TextStyle.short
)
# Rest games input
self.rest_games = discord.ui.TextInput(
label="Pitcher Rest Games",
placeholder="Enter number of rest games (0 or more)",
required=True,
max_length=2,
style=discord.TextStyle.short
)
self.add_item(self.current_week)
self.add_item(self.current_game)
self.add_item(self.rest_games)
async def on_submit(self, interaction: discord.Interaction):
"""Handle pitcher rest input and log injury."""
from services.player_service import player_service
from services.injury_service import injury_service
from models.injury import Injury
import math
# Validate current week
try:
week = int(self.current_week.value)
if week < 1 or week > 18:
raise ValueError("Week must be between 1 and 18")
except ValueError:
embed = EmbedTemplate.error(
title="Invalid Week",
description="Current week must be a number between 1 and 18."
)
await interaction.response.send_message(embed=embed, ephemeral=True)
return
# Validate current game
try:
game = int(self.current_game.value)
if game < 1 or game > 4:
raise ValueError("Game must be between 1 and 4")
except ValueError:
embed = EmbedTemplate.error(
title="Invalid Game",
description="Current game must be a number between 1 and 4."
)
await interaction.response.send_message(embed=embed, ephemeral=True)
return
# Validate rest games
try:
rest = int(self.rest_games.value)
if rest < 0:
raise ValueError("Rest games cannot be negative")
except ValueError:
embed = EmbedTemplate.error(
title="Invalid Rest Games",
description="Rest games must be a non-negative number."
)
await interaction.response.send_message(embed=embed, ephemeral=True)
return
# Calculate total injury
total_injury_games = self.injury_games + rest
# Calculate injury dates
out_weeks = math.floor(total_injury_games / 4)
out_games = total_injury_games % 4
return_week = week + out_weeks
return_game = game + 1 + out_games
if return_game > 4:
return_week += 1
return_game -= 4
# Adjust start date if injury starts after game 4
start_week = week if game != 4 else week + 1
start_game = game + 1 if game != 4 else 1
return_date = f'w{return_week:02d}g{return_game}'
# Create injury record
try:
injury = await injury_service.create_injury(
season=self.season,
player_id=self.player.id,
total_games=total_injury_games,
start_week=start_week,
start_game=start_game,
end_week=return_week,
end_game=return_game
)
if not injury:
raise ValueError("Failed to create injury record")
# Update player's il_return field
await player_service.update_player(self.player.id, {'il_return': return_date})
# Success response
embed = EmbedTemplate.success(
title="Injury Logged",
description=f"{self.player.name}'s injury has been logged."
)
embed.add_field(
name="Base Injury",
value=f"{self.injury_games} game{'s' if self.injury_games > 1 else ''}",
inline=True
)
embed.add_field(
name="Rest Requirement",
value=f"{rest} game{'s' if rest > 1 else ''}",
inline=True
)
embed.add_field(
name="Total Duration",
value=f"{total_injury_games} game{'s' if total_injury_games > 1 else ''}",
inline=True
)
embed.add_field(
name="Return Date",
value=return_date,
inline=True
)
if self.player.team:
embed.add_field(
name="Team",
value=f"{self.player.team.lname} ({self.player.team.abbrev})",
inline=False
)
self.is_submitted = True
self.result = {
'injury_id': injury.id,
'total_games': total_injury_games,
'return_date': return_date
}
await interaction.response.send_message(embed=embed)
except Exception as e:
self.logger.error("Failed to create pitcher injury", error=e, player_id=self.player.id)
embed = EmbedTemplate.error(
title="Error",
description="Failed to log the injury. Please try again or contact an administrator."
)
await interaction.response.send_message(embed=embed, ephemeral=True)

395
views/players.py Normal file
View File

@ -0,0 +1,395 @@
"""
Player View Components
Interactive Discord UI components for player information display with toggleable statistics.
"""
from typing import Optional, TYPE_CHECKING
import discord
from discord.ext import commands
from utils.logging import get_contextual_logger
from views.base import BaseView
from views.embeds import EmbedTemplate, EmbedColors
from models.team import RosterType
if TYPE_CHECKING:
from models.player import Player
from models.batting_stats import BattingStats
from models.pitching_stats import PitchingStats
class PlayerStatsView(BaseView):
"""
Interactive view for player information with toggleable batting and pitching statistics.
Features:
- Basic player info always visible
- Batting stats hidden by default, toggled with button
- Pitching stats hidden by default, toggled with button
- Buttons only appear if corresponding stats exist
- User restriction - only command caller can toggle
- 5 minute timeout with graceful cleanup
"""
def __init__(
self,
player: 'Player',
season: int,
batting_stats: Optional['BattingStats'] = None,
pitching_stats: Optional['PitchingStats'] = None,
user_id: Optional[int] = None
):
"""
Initialize the player stats view.
Args:
player: Player model with basic information
season: Season for statistics display
batting_stats: Batting statistics (if available)
pitching_stats: Pitching statistics (if available)
user_id: Discord user ID who can interact with this view
"""
super().__init__(timeout=300.0, user_id=user_id, logger_name=f'{__name__}.PlayerStatsView')
self.player = player
self.season = season
self.batting_stats = batting_stats
self.pitching_stats = pitching_stats
self.show_batting = False
self.show_pitching = False
# Only show batting button if stats are available
if not batting_stats:
self.remove_item(self.toggle_batting_button)
self.logger.debug("No batting stats available, batting button hidden")
# Only show pitching button if stats are available
if not pitching_stats:
self.remove_item(self.toggle_pitching_button)
self.logger.debug("No pitching stats available, pitching button hidden")
self.logger.info("PlayerStatsView initialized",
player_id=player.id,
player_name=player.name,
season=season,
has_batting=bool(batting_stats),
has_pitching=bool(pitching_stats),
user_id=user_id)
@discord.ui.button(
label="Show Batting Stats",
style=discord.ButtonStyle.primary,
emoji="💥",
row=0
)
async def toggle_batting_button(
self,
interaction: discord.Interaction,
button: discord.ui.Button
):
"""Toggle batting statistics visibility."""
self.increment_interaction_count()
self.show_batting = not self.show_batting
# Update button label
button.label = "Hide Batting Stats" if self.show_batting else "Show Batting Stats"
self.logger.info("Batting stats toggled",
player_id=self.player.id,
show_batting=self.show_batting,
user_id=interaction.user.id)
# Rebuild and update embed
await self._update_embed(interaction)
@discord.ui.button(
label="Show Pitching Stats",
style=discord.ButtonStyle.primary,
emoji="",
row=0
)
async def toggle_pitching_button(
self,
interaction: discord.Interaction,
button: discord.ui.Button
):
"""Toggle pitching statistics visibility."""
self.increment_interaction_count()
self.show_pitching = not self.show_pitching
# Update button label
button.label = "Hide Pitching Stats" if self.show_pitching else "Show Pitching Stats"
self.logger.info("Pitching stats toggled",
player_id=self.player.id,
show_pitching=self.show_pitching,
user_id=interaction.user.id)
# Rebuild and update embed
await self._update_embed(interaction)
async def _update_embed(self, interaction: discord.Interaction):
"""
Rebuild the player embed with current visibility settings and update the message.
Args:
interaction: Discord interaction from button click
"""
try:
# Create embed with current visibility state
embed = await self._create_player_embed()
# Update the message with new embed
await interaction.response.edit_message(embed=embed, view=self)
self.logger.debug("Embed updated successfully",
show_batting=self.show_batting,
show_pitching=self.show_pitching)
except Exception as e:
self.logger.error("Failed to update embed", error=str(e), exc_info=True)
# Try to send error message
try:
error_embed = EmbedTemplate.error(
title="Update Failed",
description="Failed to update player statistics. Please try again."
)
await interaction.response.send_message(embed=error_embed, ephemeral=True)
except Exception:
self.logger.error("Failed to send error message", exc_info=True)
async def _create_player_embed(self) -> discord.Embed:
"""
Create player embed with current visibility settings.
Returns:
Discord embed with player information and visible stats
"""
player = self.player
season = self.season
# Determine embed color based on team
embed_color = EmbedColors.PRIMARY
if hasattr(player, 'team') and player.team and hasattr(player.team, 'color'):
try:
# Convert hex color string to int
embed_color = int(player.team.color, 16)
except (ValueError, TypeError):
embed_color = EmbedColors.PRIMARY
# Create base embed with player name as title
# Add injury indicator emoji if player is injured
title = f"🤕 {player.name}" if player.il_return is not None else player.name
embed = EmbedTemplate.create_base_embed(
title=title,
color=embed_color
)
# Basic info section (always visible)
embed.add_field(
name="Position",
value=player.primary_position,
inline=True
)
if hasattr(player, 'team') and player.team:
embed.add_field(
name="Team",
value=f"{player.team.abbrev} - {player.team.sname}",
inline=True
)
# Add Major League affiliate if this is a Minor League team
if player.team.roster_type() == RosterType.MINOR_LEAGUE:
major_affiliate = player.team.get_major_league_affiliate()
if major_affiliate:
embed.add_field(
name="Major Affiliate",
value=major_affiliate,
inline=True
)
embed.add_field(
name="sWAR",
value=f"{player.wara:.1f}",
inline=True
)
embed.add_field(
name="Player ID",
value=str(player.id),
inline=True
)
# All positions if multiple
if len(player.positions) > 1:
embed.add_field(
name="Positions",
value=", ".join(player.positions),
inline=True
)
embed.add_field(
name="Season",
value=str(season),
inline=True
)
# Always show injury rating
embed.add_field(
name="Injury Rating",
value=player.injury_rating or "N/A",
inline=True
)
# Show injury return date only if player is currently injured
if player.il_return:
embed.add_field(
name="Injury Return",
value=player.il_return,
inline=True
)
# Add batting stats if visible and available
if self.show_batting and self.batting_stats:
embed.add_field(name='', value='', inline=False)
self.logger.debug("Adding batting statistics to embed")
batting_stats = self.batting_stats
rate_stats = (
"```\n"
"╭─────────────╮\n"
f"│ AVG {batting_stats.avg:.3f}\n"
f"│ OBP {batting_stats.obp:.3f}\n"
f"│ SLG {batting_stats.slg:.3f}\n"
f"│ OPS {batting_stats.ops:.3f}\n"
f"│ wOBA {batting_stats.woba:.3f}\n"
"╰─────────────╯\n"
"```"
)
embed.add_field(
name="Rate Stats",
value=rate_stats,
inline=True
)
count_stats = (
"```\n"
"╭───────────╮\n"
f"│ HR {batting_stats.homerun:>3}\n"
f"│ RBI {batting_stats.rbi:>3}\n"
f"│ R {batting_stats.run:>3}\n"
f"│ AB {batting_stats.ab:>4}\n"
f"│ H {batting_stats.hit:>4}\n"
f"│ BB {batting_stats.bb:>3}\n"
f"│ SO {batting_stats.so:>3}\n"
"╰───────────╯\n"
"```"
)
embed.add_field(
name='Counting Stats',
value=count_stats,
inline=True
)
# Add pitching stats if visible and available
if self.show_pitching and self.pitching_stats:
embed.add_field(name='', value='', inline=False)
self.logger.debug("Adding pitching statistics to embed")
pitching_stats = self.pitching_stats
ip = pitching_stats.innings_pitched
record_stats = (
"```\n"
"╭─────────────╮\n"
f"│ G-GS {pitching_stats.games:>2}-{pitching_stats.gs:<2}\n"
f"│ W-L {pitching_stats.win:>2}-{pitching_stats.loss:<2}\n"
f"│ H-SV {pitching_stats.hold:>2}-{pitching_stats.saves:<2}\n"
f"│ ERA {pitching_stats.era:>5.2f}\n"
f"│ WHIP {pitching_stats.whip:>5.2f}\n"
"╰─────────────╯\n"
"```"
)
embed.add_field(
name="Record Stats",
value=record_stats,
inline=True
)
strikeout_stats = (
"```\n"
"╭──────────╮\n"
f"│ IP{ip:>6.1f}\n"
f"│ SO {pitching_stats.so:>3}\n"
f"│ BB {pitching_stats.bb:>3}\n"
f"│ H {pitching_stats.hits:>3}\n"
"╰──────────╯\n"
"```"
)
embed.add_field(
name='Counting Stats',
value=strikeout_stats,
inline=True
)
# Add a note if no stats are visible
if not self.show_batting and not self.show_pitching:
if self.batting_stats or self.pitching_stats:
embed.add_field(
name="📊 Statistics",
value="Click the buttons below to show statistics.",
inline=False
)
else:
embed.add_field(
name="📊 Statistics",
value="No statistics available for this season.",
inline=False
)
# Set player card as main image
if player.image:
embed.set_image(url=player.image)
self.logger.debug("Player card image added to embed", image_url=player.image)
# Set thumbnail with priority: fancycard → headshot → team logo
thumbnail_url = None
thumbnail_source = None
if hasattr(player, 'vanity_card') and player.vanity_card:
thumbnail_url = player.vanity_card
thumbnail_source = "fancycard"
elif hasattr(player, 'headshot') and player.headshot:
thumbnail_url = player.headshot
thumbnail_source = "headshot"
elif hasattr(player, 'team') and player.team and hasattr(player.team, 'thumbnail') and player.team.thumbnail:
thumbnail_url = player.team.thumbnail
thumbnail_source = "team logo"
if thumbnail_url:
embed.set_thumbnail(url=thumbnail_url)
self.logger.debug(f"Thumbnail set from {thumbnail_source}", thumbnail_url=thumbnail_url)
# Footer with player ID
footer_text = f"Player ID: {player.id}"
embed.set_footer(text=footer_text)
return embed
async def get_initial_embed(self) -> discord.Embed:
"""
Get the initial embed with stats hidden.
Returns:
Discord embed with player information, stats hidden by default
"""
# Ensure stats are hidden for initial display
self.show_batting = False
self.show_pitching = False
return await self._create_player_embed()