major-domo-v2/views/players.py
2025-10-21 10:58:08 -05:00

394 lines
14 KiB
Python

"""
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=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 player.team and player.team.color:
embed_color = int(player.team.color, 16)
else:
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.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()