CLAUDE: Add /jump command and improve dice rolling with team colors, plus admin and type safety fixes

Added /jump command for baserunner stealing mechanics with pickoff/balk detection.
Enhanced dice rolling commands with team color support in embeds.
Improved /admin-sync with local/global options and prefix command fallback.
Fixed type safety issues in admin commands and injury management.
Updated config for expanded draft rounds and testing mode.

Key changes:
- commands/dice/rolls.py: New /jump and !j commands with special cases for pickoff (d20=1) and balk (d20=2)
- commands/dice/rolls.py: Added team/channel color support to /ab and dice embeds
- commands/dice/rolls.py: Added pitcher position to /fielding command with proper range/error charts
- commands/admin/management.py: Enhanced /admin-sync with local/clear options and !admin-sync prefix fallback
- commands/admin/management.py: Fixed Member type checking and channel type validation
- commands/injuries/management.py: Fixed responder team detection for injury clearing
- models/custom_command.py: Made creator_id optional for execute endpoint compatibility
- config.py: Updated draft_rounds to 32 and enabled testing mode
- services/transaction_builder.py: Adjusted ML roster limit to 26

🤖 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-20 15:10:48 -05:00
parent 62c658fb57
commit 1575d4f096
7 changed files with 369 additions and 48 deletions

6
bot.py
View File

@ -93,7 +93,7 @@ class SBABot(commands.Bot):
# Initialize cleanup tasks # Initialize cleanup tasks
await self._setup_background_tasks() await self._setup_background_tasks()
# Smart command syncing: auto-sync in development if changes detected # Smart command syncing: auto-sync in development if changes detected; !admin-sync for first sync
config = get_config() config = get_config()
if config.is_development: if config.is_development:
if await self._should_sync_commands(): if await self._should_sync_commands():
@ -104,7 +104,7 @@ class SBABot(commands.Bot):
self.logger.info("Development mode: no command changes detected, skipping sync") self.logger.info("Development mode: no command changes detected, skipping sync")
else: else:
self.logger.info("Production mode: commands loaded but not auto-synced") self.logger.info("Production mode: commands loaded but not auto-synced")
self.logger.info("Use /sync command to manually sync when needed") self.logger.info("Use /admin-sync command to manually sync when needed")
async def _load_command_packages(self): async def _load_command_packages(self):
"""Load all command packages with resilient error handling.""" """Load all command packages with resilient error handling."""
@ -290,7 +290,7 @@ class SBABot(commands.Bot):
async def _sync_commands(self): async def _sync_commands(self):
"""Internal method to sync commands.""" """Internal method to sync commands."""
config = get_config() config = get_config()
if config.guild_id: if config.testing and config.guild_id:
guild = discord.Object(id=config.guild_id) guild = discord.Object(id=config.guild_id)
self.tree.copy_global_to(guild=guild) self.tree.copy_global_to(guild=guild)
synced = await self.tree.sync(guild=guild) synced = await self.tree.sync(guild=guild)

View File

@ -3,7 +3,6 @@ Admin Management Commands
Administrative commands for league management and bot maintenance. Administrative commands for league management and bot maintenance.
""" """
from typing import Optional, Union
import asyncio import asyncio
import discord import discord
@ -25,6 +24,14 @@ class AdminCommands(commands.Cog):
async def interaction_check(self, interaction: discord.Interaction) -> bool: async def interaction_check(self, interaction: discord.Interaction) -> bool:
"""Check if user has admin permissions.""" """Check if user has admin permissions."""
# Check if interaction is from a guild and user is a Member
if not isinstance(interaction.user, discord.Member):
await interaction.response.send_message(
"❌ Admin commands can only be used in a server.",
ephemeral=True
)
return False
if not interaction.user.guild_permissions.administrator: if not interaction.user.guild_permissions.administrator:
await interaction.response.send_message( await interaction.response.send_message(
"❌ You need administrator permissions to use admin commands.", "❌ You need administrator permissions to use admin commands.",
@ -196,11 +203,102 @@ class AdminCommands(commands.Cog):
name="admin-sync", name="admin-sync",
description="Sync application commands with Discord" description="Sync application commands with Discord"
) )
@app_commands.describe(
local="Sync to this guild only (fast, for development)",
clear_local="Clear locally synced commands (does not sync after clearing)"
)
@logged_command("/admin-sync") @logged_command("/admin-sync")
async def admin_sync(self, interaction: discord.Interaction): async def admin_sync(
self,
interaction: discord.Interaction,
local: bool = False,
clear_local: bool = False
):
"""Sync slash commands with Discord API.""" """Sync slash commands with Discord API."""
await interaction.response.defer() await interaction.response.defer()
try:
# Clear local commands if requested
if clear_local:
if not interaction.guild_id:
raise ValueError("Cannot clear local commands outside of a guild")
self.logger.info(f"Clearing local commands for guild {interaction.guild_id}")
self.bot.tree.clear_commands(guild=discord.Object(id=interaction.guild_id))
embed = EmbedTemplate.create_base_embed(
title="✅ Local Commands Cleared",
description=f"Cleared all commands synced to this guild",
color=EmbedColors.SUCCESS
)
embed.add_field(
name="Clear Details",
value=f"**Guild ID:** {interaction.guild_id}\n"
f"**Time:** {discord.utils.utcnow().strftime('%H:%M:%S UTC')}\n"
f"**Note:** Commands not synced after clearing",
inline=False
)
await interaction.followup.send(embed=embed)
return
# Determine sync target
if local:
if not interaction.guild_id:
raise ValueError("Cannot sync locally outside of a guild")
guild = discord.Object(id=interaction.guild_id)
sync_type = "local guild"
else:
guild = None
sync_type = "globally"
# Perform sync
self.logger.info(f"Syncing commands {sync_type}")
synced_commands = await self.bot.tree.sync(guild=guild)
embed = EmbedTemplate.create_base_embed(
title="✅ Commands Synced Successfully",
description=f"Synced {len(synced_commands)} application commands {sync_type}",
color=EmbedColors.SUCCESS
)
# Show some of the synced commands
command_names = [cmd.name for cmd in synced_commands[:10]]
embed.add_field(
name="Synced Commands",
value="\n".join([f"• /{name}" for name in command_names]) +
(f"\n... and {len(synced_commands) - 10} more" if len(synced_commands) > 10 else ""),
inline=False
)
embed.add_field(
name="Sync Details",
value=f"**Total Commands:** {len(synced_commands)}\n"
f"**Sync Type:** {sync_type.title()}\n"
f"**Guild ID:** {interaction.guild_id or 'N/A'}\n"
f"**Time:** {discord.utils.utcnow().strftime('%H:%M:%S UTC')}",
inline=False
)
except Exception as e:
self.logger.error(f"Sync failed: {e}", exc_info=True)
embed = EmbedTemplate.create_base_embed(
title="❌ Sync Failed",
description=f"Failed to sync commands: {str(e)}",
color=EmbedColors.ERROR
)
await interaction.followup.send(embed=embed)
@commands.command(name="admin-sync")
@commands.has_permissions(administrator=True)
async def admin_sync_prefix(self, ctx: commands.Context):
"""
Prefix command version of admin-sync for bootstrap scenarios.
Use this when slash commands aren't synced yet and you can't access /admin-sync.
"""
self.logger.info(f"Prefix command !admin-sync invoked by {ctx.author} in {ctx.guild}")
try: try:
synced_commands = await self.bot.tree.sync() synced_commands = await self.bot.tree.sync()
@ -222,19 +320,22 @@ class AdminCommands(commands.Cog):
embed.add_field( embed.add_field(
name="Sync Details", name="Sync Details",
value=f"**Total Commands:** {len(synced_commands)}\n" value=f"**Total Commands:** {len(synced_commands)}\n"
f"**Guild ID:** {interaction.guild_id}\n" f"**Guild ID:** {ctx.guild.id}\n"
f"**Time:** {discord.utils.utcnow().strftime('%H:%M:%S UTC')}", f"**Time:** {discord.utils.utcnow().strftime('%H:%M:%S UTC')}",
inline=False inline=False
) )
embed.set_footer(text="💡 Use /admin-sync (slash command) for future syncs")
except Exception as e: except Exception as e:
self.logger.error(f"Prefix command sync failed: {e}", exc_info=True)
embed = EmbedTemplate.create_base_embed( embed = EmbedTemplate.create_base_embed(
title="❌ Sync Failed", title="❌ Sync Failed",
description=f"Failed to sync commands: {str(e)}", description=f"Failed to sync commands: {str(e)}",
color=EmbedColors.ERROR color=EmbedColors.ERROR
) )
await interaction.followup.send(embed=embed) await ctx.send(embed=embed)
@app_commands.command( @app_commands.command(
name="admin-clear", name="admin-clear",
@ -255,6 +356,14 @@ class AdminCommands(commands.Cog):
await interaction.response.defer() await interaction.response.defer()
# Verify channel type supports purge
if not isinstance(interaction.channel, (discord.TextChannel, discord.Thread, discord.VoiceChannel, discord.StageChannel)):
await interaction.followup.send(
"❌ Cannot purge messages in this channel type.",
ephemeral=True
)
return
try: try:
deleted = await interaction.channel.purge(limit=count) deleted = await interaction.channel.purge(limit=count)
@ -276,6 +385,7 @@ class AdminCommands(commands.Cog):
# Send confirmation and auto-delete after 5 seconds # Send confirmation and auto-delete after 5 seconds
message = await interaction.followup.send(embed=embed) message = await interaction.followup.send(embed=embed)
await asyncio.sleep(5) await asyncio.sleep(5)
if message:
try: try:
await message.delete() await message.delete()
except discord.NotFound: except discord.NotFound:
@ -321,9 +431,11 @@ class AdminCommands(commands.Cog):
icon_url=interaction.user.display_avatar.url icon_url=interaction.user.display_avatar.url
) )
content = "@everyone" if mention_everyone else None # Send with or without mention based on flag
if mention_everyone:
await interaction.followup.send(content=content, embed=embed) await interaction.followup.send(content="@everyone", embed=embed)
else:
await interaction.followup.send(embed=embed)
# Log the announcement # Log the announcement
self.logger.info( self.logger.info(

View File

@ -11,8 +11,12 @@ from dataclasses import dataclass
import discord import discord
from discord.ext import commands from discord.ext import commands
from models.team import Team
from services.team_service import team_service
from utils import team_utils
from utils.logging import get_contextual_logger from utils.logging import get_contextual_logger
from utils.decorators import logged_command from utils.decorators import logged_command
from utils.team_utils import get_user_major_league_team
from views.embeds import EmbedColors, EmbedTemplate from views.embeds import EmbedColors, EmbedTemplate
@ -93,13 +97,20 @@ class DiceRollCommands(commands.Cog):
async def ab_dice(self, interaction: discord.Interaction): async def ab_dice(self, interaction: discord.Interaction):
"""Roll the standard baseball at-bat dice combination.""" """Roll the standard baseball at-bat dice combination."""
await interaction.response.defer() await interaction.response.defer()
embed_color = await self._get_channel_embed_color(interaction)
# Use the standard baseball dice combination # Use the standard baseball dice combination
dice_notation = "1d6;2d6;1d20" dice_notation = "1d6;2d6;1d20"
roll_results = self._parse_and_roll_multiple_dice(dice_notation) roll_results = self._parse_and_roll_multiple_dice(dice_notation)
# Create embed for the roll results # Create embed for the roll results
embed = self._create_multi_roll_embed(dice_notation, roll_results, interaction.user, set_author=False) embed = self._create_multi_roll_embed(
dice_notation,
roll_results,
interaction.user,
set_author=False,
embed_color=embed_color
)
embed.title = f'At bat roll for {interaction.user.display_name}' embed.title = f'At bat roll for {interaction.user.display_name}'
await interaction.followup.send(embed=embed) await interaction.followup.send(embed=embed)
@ -107,6 +118,10 @@ class DiceRollCommands(commands.Cog):
async def ab_dice_prefix(self, ctx: commands.Context): async def ab_dice_prefix(self, ctx: commands.Context):
"""Roll baseball at-bat dice using prefix commands (!ab, !atbat).""" """Roll baseball at-bat dice using prefix commands (!ab, !atbat)."""
self.logger.info(f"At Bat dice command started by {ctx.author.display_name}") self.logger.info(f"At Bat dice command started by {ctx.author.display_name}")
team = await get_user_major_league_team(user_id=ctx.author.id)
embed_color = EmbedColors.PRIMARY
if team is not None and team.color is not None:
embed_color = int(team.color,16)
# Use the standard baseball dice combination # Use the standard baseball dice combination
dice_notation = "1d6;2d6;1d20" dice_notation = "1d6;2d6;1d20"
@ -115,7 +130,7 @@ class DiceRollCommands(commands.Cog):
self.logger.info("At Bat dice rolled successfully", roll_count=len(roll_results)) self.logger.info("At Bat dice rolled successfully", roll_count=len(roll_results))
# Create embed for the roll results # Create embed for the roll results
embed = self._create_multi_roll_embed(dice_notation, roll_results, ctx.author) embed = self._create_multi_roll_embed(dice_notation, roll_results, ctx.author, set_author=False, embed_color=embed_color)
embed.title = f'At bat roll for {ctx.author.display_name}' embed.title = f'At bat roll for {ctx.author.display_name}'
await ctx.send(embed=embed) await ctx.send(embed=embed)
@ -158,6 +173,7 @@ class DiceRollCommands(commands.Cog):
position="Defensive position" position="Defensive position"
) )
@discord.app_commands.choices(position=[ @discord.app_commands.choices(position=[
discord.app_commands.Choice(name="Pitcher (P)", value="P"),
discord.app_commands.Choice(name="Catcher (C)", value="C"), discord.app_commands.Choice(name="Catcher (C)", value="C"),
discord.app_commands.Choice(name="First Base (1B)", value="1B"), discord.app_commands.Choice(name="First Base (1B)", value="1B"),
discord.app_commands.Choice(name="Second Base (2B)", value="2B"), discord.app_commands.Choice(name="Second Base (2B)", value="2B"),
@ -212,6 +228,82 @@ class DiceRollCommands(commands.Cog):
embed = self._create_fielding_embed(parsed_position, roll_results, ctx.author) embed = self._create_fielding_embed(parsed_position, roll_results, ctx.author)
await ctx.send(embed=embed) await ctx.send(embed=embed)
@discord.app_commands.command(
name="jump",
description="Roll for baserunner's jump before stealing"
)
@logged_command("/jump")
async def jump_dice(self, interaction: discord.Interaction):
"""Roll to check for a baserunner's jump before attempting to steal a base."""
await interaction.response.defer()
embed_color = await self._get_channel_embed_color(interaction)
# Roll 1d20 for pickoff/balk check
check_roll = random.randint(1, 20)
# Roll 2d6 for jump rating
jump_result = self._parse_and_roll_single_dice("2d6")
# Roll another 1d20 for pickoff/balk resolution
resolution_roll = random.randint(1, 20)
# Create embed based on check roll
embed = self._create_jump_embed(
check_roll,
jump_result,
resolution_roll,
interaction.user,
embed_color,
show_author=False
)
await interaction.followup.send(embed=embed)
@commands.command(name="j", aliases=["jump"])
async def jump_dice_prefix(self, ctx: commands.Context):
"""Roll for baserunner's jump using prefix commands (!j, !jump)."""
self.logger.info(f"Jump command started by {ctx.author.display_name}")
team = await get_user_major_league_team(user_id=ctx.author.id)
embed_color = EmbedColors.PRIMARY
if team is not None and team.color is not None:
embed_color = int(team.color, 16)
# Roll 1d20 for pickoff/balk check
check_roll = random.randint(1, 20)
# Roll 2d6 for jump rating
jump_result = self._parse_and_roll_single_dice("2d6")
# Roll another 1d20 for pickoff/balk resolution
resolution_roll = random.randint(1, 20)
self.logger.info("Jump dice rolled successfully", check=check_roll, jump=jump_result.total if jump_result else None, resolution=resolution_roll)
# Create embed based on check roll
embed = self._create_jump_embed(
check_roll,
jump_result,
resolution_roll,
ctx.author,
embed_color
)
await ctx.send(embed=embed)
async def _get_channel_embed_color(self, interaction: discord.Interaction) -> int:
# Check if channel is a type that has a name attribute (DMChannel doesn't have one)
if isinstance(interaction.channel, (discord.TextChannel, discord.VoiceChannel, discord.Thread)):
channel_starter = interaction.channel.name[:6]
if '-' in channel_starter:
abbrev = channel_starter.split('-')[0]
channel_team = await team_service.get_team_by_abbrev(abbrev)
if channel_team is not None and channel_team.color is not None:
return int(channel_team.color,16)
team = await get_user_major_league_team(user_id=interaction.user.id)
if team is not None and team.color is not None:
return int(team.color,16)
return EmbedColors.PRIMARY
def _parse_position(self, position: str) -> str | None: def _parse_position(self, position: str) -> str | None:
"""Parse and validate fielding position input for prefix commands.""" """Parse and validate fielding position input for prefix commands."""
if not position: if not position:
@ -221,6 +313,7 @@ class DiceRollCommands(commands.Cog):
# Map common inputs to standard position names # Map common inputs to standard position names
position_map = { position_map = {
'P': 'P', 'PITCHER': 'P',
'C': 'C', 'CATCHER': 'C', 'C': 'C', 'CATCHER': 'C',
'1': '1B', '1B': '1B', 'FIRST': '1B', 'FIRSTBASE': '1B', '1': '1B', '1B': '1B', 'FIRST': '1B', 'FIRSTBASE': '1B',
'2': '2B', '2B': '2B', 'SECOND': '2B', 'SECONDBASE': '2B', '2': '2B', '2B': '2B', 'SECOND': '2B', 'SECONDBASE': '2B',
@ -266,8 +359,8 @@ class DiceRollCommands(commands.Cog):
# Add fielding check summary # Add fielding check summary
range_result = self._get_range_result(position, d20_result) range_result = self._get_range_result(position, d20_result)
embed.add_field( embed.add_field(
name=f"{position} Fielding Check Summary", name=f"{position} Range Result",
value=f"```\nRange Result\n 1 | 2 | 3 | 4 | 5\n{range_result}```", value=f"```md\n 1 | 2 | 3 | 4 | 5\n{range_result}```",
inline=False inline=False
) )
@ -280,17 +373,77 @@ class DiceRollCommands(commands.Cog):
inline=False inline=False
) )
# Add help commands # # Add help commands
embed.add_field( # embed.add_field(
name="Help Commands", # name="Help Commands",
value="Run !<result> for full chart readout (e.g. !g1 or !do3)", # value="Run !<result> for full chart readout (e.g. !g1 or !do3)",
inline=False # inline=False
# )
# # Add references
# embed.add_field(
# name="References",
# value="Range Chart / Error Chart / Result Reference",
# inline=False
# )
return embed
def _create_jump_embed(
self,
check_roll: int,
jump_result: DiceRoll | None,
resolution_roll: int,
user: discord.User | discord.Member,
embed_color: int = EmbedColors.PRIMARY,
show_author: bool = True
) -> discord.Embed:
"""Create an embed for jump roll results."""
# Create base embed
embed = EmbedTemplate.create_base_embed(
title=f"Jump roll for {user.name}",
color=embed_color
) )
# Add references if show_author:
# Set user info
embed.set_author(
name=user.name,
icon_url=user.display_avatar.url
)
# Check for pickoff or balk
if check_roll == 1:
# Pickoff attempt
embed.add_field( embed.add_field(
name="References", name="Special",
value="Range Chart / Error Chart / Result Reference", value="```md\nCheck pickoff```",
inline=False
)
embed.add_field(
name="Pickoff roll",
value=f"```md\n# {resolution_roll}\nDetails:[1d20 ({resolution_roll})]```",
inline=False
)
elif check_roll == 2:
# Balk
embed.add_field(
name="Special",
value="```md\nCheck balk```",
inline=False
)
embed.add_field(
name="Balk roll",
value=f"```md\n# {resolution_roll}\nDetails:[1d20 ({resolution_roll})]```",
inline=False
)
else:
# Normal jump - show 2d6 result
if jump_result:
rolls_str = ' '.join(str(r) for r in jump_result.rolls)
embed.add_field(
name="Result",
value=f"```md\n# {jump_result.total}\nDetails:[2d6 ({rolls_str})]```",
inline=False inline=False
) )
@ -298,8 +451,9 @@ class DiceRollCommands(commands.Cog):
def _get_range_result(self, position: str, d20_roll: int) -> str: def _get_range_result(self, position: str, d20_roll: int) -> str:
"""Get the range result display for a position and d20 roll.""" """Get the range result display for a position and d20 roll."""
# Infield positions share the same range chart if position == 'P':
if position in ['1B', '2B', '3B', 'SS']: return self._get_pitcher_range(d20_roll)
elif position in ['1B', '2B', '3B', 'SS']:
return self._get_infield_range(d20_roll) return self._get_infield_range(d20_roll)
elif position in ['LF', 'CF', 'RF']: elif position in ['LF', 'CF', 'RF']:
return self._get_outfield_range(d20_roll) return self._get_outfield_range(d20_roll)
@ -385,10 +539,38 @@ class DiceRollCommands(commands.Cog):
} }
return catcher_ranges.get(d20_roll, 'Unknown') return catcher_ranges.get(d20_roll, 'Unknown')
def _get_pitcher_range(self, d20_roll: int) -> str:
"""Get pitcher range result based on d20 roll."""
pitcher_ranges = {
1: 'G3 ------SI1------',
2: 'G3 ------SI1------',
3: '--G3--- ----SI1----',
4: '----G3----- --SI1--',
5: '------G3------- SI1',
6: '------G3------- SI1',
7: '--------G3---------',
8: 'G2 ------G3-------',
9: 'G2 ------G3-------',
10: 'G1 G2 ----G3-----',
11: 'G1 G2 ----G3-----',
12: 'G1 G2 ----G3-----',
13: '--G1--- G2 --G3---',
14: '--G1--- --G2--- G3',
15: '--G1--- ----G2-----',
16: '--G1--- ----G2-----',
17: '----G1----- --G2---',
18: '----G1----- --G2---',
19: '------G1------- G2',
20: '--------G1---------'
}
return pitcher_ranges.get(d20_roll, 'Unknown')
def _get_error_result(self, position: str, d6_total: int) -> str: def _get_error_result(self, position: str, d6_total: int) -> str:
"""Get the error result for a position and 3d6 total.""" """Get the error result for a position and 3d6 total."""
# Get the appropriate error chart # Get the appropriate error chart
if position == '1B': if position == 'P':
return self._get_pitcher_error(d6_total)
elif position == '1B':
return self._get_1b_error(d6_total) return self._get_1b_error(d6_total)
elif position == '2B': elif position == '2B':
return self._get_2b_error(d6_total) return self._get_2b_error(d6_total)
@ -560,6 +742,28 @@ class DiceRollCommands(commands.Cog):
} }
return errors.get(d6_total, 'No error') return errors.get(d6_total, 'No error')
def _get_pitcher_error(self, d6_total: int) -> str:
"""Get Pitcher error result based on 3d6 total."""
errors = {
18: '2-base error for e4 -> e12, e19 -> e28, e34 -> e43, e46 -> e48',
17: '2-base error for e13 -> e28, e44 -> e50',
16: '2-base error for e30 -> e48, e50, e51\n1-base error for e8, e11, e16, e23',
15: '2-base error for e50, e51\n1-base error for e10 -> e12, e19, e20, e24, e26, e30, e35, e38, e40, e46, e47',
14: '1-base error for e4, e14, e18, e21, e22, e26, e31, e35, e42, e43, e48 -> e51',
13: '1-base error for e6, e13, e14, e21, e22, e26, e27, e30 -> 34, e38 -> e51',
12: '1-base error for e7, e11, e12, e15 -> e19, e22 -> e51',
11: '1-base error for e10, e13, e15, e17, e18, e20, e21, e23, e24, e27 -> 38, e40, e42, e44 -> e51',
10: '1-base error for e20, e23, e24, e27 -> e51',
9: '1-base error for e16, e19, e26, e28, e34 -> e36, e39 -> e51',
8: '1-base error for e22, e33, e38, e39, e43 -> e51',
7: '1-base error for e14, e21, e36, e39, e42 -> e44, e47 -> e51',
6: '1-base error for e8, e22, e38, e39, e43 -> e51',
5: 'No error',
4: '1-base error for e15, e16, e40',
3: '2-base error for e8 -> e12, e26 -> e28, e39 -> e43\n1-base error for e2, e3, e7, e14, e15'
}
return errors.get(d6_total, 'No error')
def _parse_and_roll_multiple_dice(self, dice_notation: str) -> list[DiceRoll]: def _parse_and_roll_multiple_dice(self, dice_notation: str) -> list[DiceRoll]:
"""Parse dice notation (supports multiple rolls) and return roll results.""" """Parse dice notation (supports multiple rolls) and return roll results."""
# Split by semicolon for multiple rolls # Split by semicolon for multiple rolls
@ -637,11 +841,11 @@ class DiceRollCommands(commands.Cog):
return [first_d6_result, second_result, third_result] return [first_d6_result, second_result, third_result]
def _create_multi_roll_embed(self, dice_notation: str, roll_results: list[DiceRoll], user: discord.User | discord.Member, set_author: bool = True) -> discord.Embed: def _create_multi_roll_embed(self, dice_notation: str, roll_results: list[DiceRoll], user: discord.User | discord.Member, set_author: bool = True, embed_color: int = EmbedColors.PRIMARY) -> discord.Embed:
"""Create an embed for multiple dice roll results.""" """Create an embed for multiple dice roll results."""
embed = EmbedTemplate.create_base_embed( embed = EmbedTemplate.create_base_embed(
title="🎲 Dice Roll", title="🎲 Dice Roll",
color=EmbedColors.PRIMARY color=embed_color
) )
if set_author: if set_author:

View File

@ -20,10 +20,12 @@ from config import get_config
from models.current import Current from models.current import Current
from models.injury import Injury from models.injury import Injury
from models.player import Player from models.player import Player
from models.team import RosterType
from services.player_service import player_service from services.player_service import player_service
from services.injury_service import injury_service from services.injury_service import injury_service
from services.league_service import league_service from services.league_service import league_service
from services.giphy_service import GiphyService from services.giphy_service import GiphyService
from utils import team_utils
from utils.logging import get_contextual_logger from utils.logging import get_contextual_logger
from utils.decorators import logged_command from utils.decorators import logged_command
from utils.autocomplete import player_autocomplete from utils.autocomplete import player_autocomplete
@ -608,6 +610,9 @@ class InjuryGroup(app_commands.Group):
inline=True inline=True
) )
if player.team.roster_type() != RosterType.MAJOR_LEAGUE:
responder_team = await team_utils.get_user_major_league_team(interaction.user.id)
# Create callback for confirmation # Create callback for confirmation
async def clear_confirm_callback(button_interaction: discord.Interaction): async def clear_confirm_callback(button_interaction: discord.Interaction):
"""Handle confirmation to clear injury.""" """Handle confirmation to clear injury."""
@ -666,7 +671,7 @@ class InjuryGroup(app_commands.Group):
view = ConfirmationView( view = ConfirmationView(
user_id=interaction.user.id, user_id=interaction.user.id,
timeout=180.0, # 3 minutes for confirmation timeout=180.0, # 3 minutes for confirmation
responders=[player.team.gmid, player.team.gmid2] if player.team else None, responders=[responder_team.gmid, responder_team.gmid2] if responder_team else None,
confirm_callback=clear_confirm_callback, confirm_callback=clear_confirm_callback,
confirm_label="Clear Injury", confirm_label="Clear Injury",
cancel_label="Cancel" cancel_label="Cancel"

View File

@ -46,7 +46,7 @@ class BotConfig(BaseSettings):
# Draft Constants # Draft Constants
default_pick_minutes: int = 10 default_pick_minutes: int = 10
draft_rounds: int = 25 draft_rounds: int = 32
# Special Team IDs # Special Team IDs
free_agent_team_id: int = 498 free_agent_team_id: int = 498
@ -64,7 +64,7 @@ class BotConfig(BaseSettings):
# Application settings # Application settings
log_level: str = "INFO" log_level: str = "INFO"
environment: str = "development" environment: str = "development"
testing: bool = False testing: bool = True
# Google Sheets settings # Google Sheets settings
sheets_credentials_path: str = "/app/data/major-domo-service-creds.json" sheets_credentials_path: str = "/app/data/major-domo-service-creds.json"

View File

@ -27,7 +27,7 @@ class CustomCommand(SBABaseModel):
id: int = Field(..., description="Database ID") # type: ignore id: int = Field(..., description="Database ID") # type: ignore
name: str = Field(..., description="Command name (unique)") name: str = Field(..., description="Command name (unique)")
content: str = Field(..., description="Command response content") content: str = Field(..., description="Command response content")
creator_id: int = Field(..., description="ID of the creator") creator_id: Optional[int] = Field(None, description="ID of the creator (may be missing from execute endpoint)")
creator: Optional[CustomCommandCreator] = Field(None, description="Creator details") creator: Optional[CustomCommandCreator] = Field(None, description="Creator details")
# Timestamps # Timestamps

View File

@ -380,7 +380,7 @@ class TransactionBuilder:
ml_limit = 26 ml_limit = 26
mil_limit = 6 mil_limit = 6
else: else:
ml_limit = 25 ml_limit = 26
mil_limit = 14 mil_limit = 14
# Validate roster limits # Validate roster limits