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:
parent
62c658fb57
commit
1575d4f096
6
bot.py
6
bot.py
@ -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)
|
||||||
|
|||||||
@ -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,10 +385,11 @@ 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)
|
||||||
try:
|
if message:
|
||||||
await message.delete()
|
try:
|
||||||
except discord.NotFound:
|
await message.delete()
|
||||||
pass # Message already deleted
|
except discord.NotFound:
|
||||||
|
pass # Message already deleted
|
||||||
|
|
||||||
except discord.Forbidden:
|
except discord.Forbidden:
|
||||||
await interaction.followup.send(
|
await interaction.followup.send(
|
||||||
@ -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(
|
||||||
|
|||||||
@ -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,26 +373,87 @@ 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:
|
||||||
embed.add_field(
|
# Set user info
|
||||||
name="References",
|
embed.set_author(
|
||||||
value="Range Chart / Error Chart / Result Reference",
|
name=user.name,
|
||||||
inline=False
|
icon_url=user.display_avatar.url
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Check for pickoff or balk
|
||||||
|
if check_roll == 1:
|
||||||
|
# Pickoff attempt
|
||||||
|
embed.add_field(
|
||||||
|
name="Special",
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
return embed
|
return embed
|
||||||
|
|
||||||
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:
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user