"""Views for the crime system.""" import discord import random from redbot.core import commands, bank from redbot.core.i18n import Translator from redbot.core.utils.chat_formatting import humanize_number from typing import Optional, Tuple from ..utils import ( calculate_stolen_amount, can_target_user, format_cooldown_time, update_streak, format_streak_text ) import asyncio from .scenarios import get_random_scenario, get_crime_event, format_text, get_all_scenarios _ = Translator("Crime", __file__) class CrimeButton(discord.ui.Button): """A button for committing crimes""" def __init__(self, style: discord.ButtonStyle, label: str, emoji: str, custom_id: str, disabled: bool = False): super().__init__( style=style, label=label, emoji=emoji, custom_id=custom_id, disabled=disabled ) async def callback(self, interaction: discord.Interaction): """Handle button press""" if interaction.user.bot: return view: CrimeListView = self.view try: await interaction.response.defer() # Get crime data crime_type = self.custom_id crime_data = view.crime_options[crime_type] # Check if user is in jail jail_remaining = await view.cog.get_jail_time_remaining(interaction.user) if jail_remaining > 0: await interaction.channel.send( _("โ›“๏ธ You're still in jail for {minutes}m {seconds}s! You can pay bail using `!crime bail` or jailbreak using `!crime jailbreak`").format( minutes=jail_remaining // 60, seconds=jail_remaining % 60 ) ) return # Check cooldown remaining = await view.cog.get_remaining_cooldown(interaction.user, crime_type) if remaining > 0: if remaining > 3600: # If more than 1 hour hours = remaining // 3600 minutes = (remaining % 3600) // 60 await interaction.channel.send( _("โณ You must wait {hours}h {minutes}m before attempting {crime_type} again!").format( hours=hours, minutes=minutes, crime_type=crime_type.replace('_', ' ').title() ) ) else: await interaction.channel.send( _("โณ You must wait {minutes}m {seconds}s before attempting {crime_type} again!").format( minutes=remaining // 60, seconds=remaining % 60, crime_type=crime_type.replace('_', ' ').title() ) ) return # Get settings settings = await view.cog.config.guild(interaction.guild).global_settings() # Delete the crime list message since we're moving to confirmation # Only delete if we're past the cooldown and jail checks try: await view.message.delete() except (discord.NotFound, discord.HTTPException): pass # If crime requires target, show target selection if crime_data.get("requires_target", False): target_view = TargetSelectionView(view.cog, interaction, crime_type, crime_data) message = await interaction.channel.send( _("Choose your target:"), view=target_view ) target_view.message = message target_view.all_messages.append(message) # Track message else: # Show confirmation for non-targeted crime crime_view = CrimeView(view.cog, interaction, crime_type, crime_data) # Format message based on crime type if crime_type == "random": embed = discord.Embed( title="๐ŸŽฒ Random Crime", description="Are you feeling lucky?", color=discord.Color.red() ) embed.add_field(name="๐Ÿ“Š Success Rate", value="???", inline=True) embed.add_field(name="๐Ÿ’ธ Potential Fine", value="???", inline=True) else: embed = discord.Embed( title=f"{crime_data.get('emoji', '๐Ÿฆน')} {crime_type.replace('_', ' ').title()}", description="Your move, boss. You ready?", color=discord.Color.red() ) embed.add_field( name="๐Ÿ“Š Success Rate", value=f"{int(crime_data['success_rate'] * 100)}%", inline=True ) embed.add_field( name="๐Ÿ’ธ Potential Fine", value=f"{int(crime_data['max_reward'] * crime_data['fine_multiplier']):,} {await bank.get_currency_name(interaction.guild)}", inline=True ) message = await interaction.channel.send( embed=embed, view=crime_view ) crime_view.message = message crime_view.all_messages = [message] # Track message except Exception as e: await interaction.channel.send( _("An error occurred while processing your crime. Please try again. Error: {error}").format( error=str(e) ) ) class CrimeListView(discord.ui.View): """View for listing and selecting available crimes. Displays crime options as buttons with: โ€ข Color-coded risk levels: - Green: Low risk crimes - Blue: Medium risk crimes - Red: High risk crimes โ€ข Crime-specific emojis: - ๐Ÿงค Pickpocket - ๐Ÿ”ช Mugging - ๐Ÿช Store Robbery - ๐Ÿ› Bank Heist - ๐ŸŽฒ Random Crime Buttons are dynamically enabled/disabled based on: โ€ข User's jail status โ€ข Individual crime cooldowns โ€ข Crime-specific requirements """ def __init__(self, cog, ctx: commands.Context, crime_options: dict): super().__init__(timeout=60) # 1 minute timeout self.cog = cog self.ctx = ctx self.crime_options = crime_options self.message = None self.all_messages = [] # Track all messages # Add buttons for each crime for crime_type, data in crime_options.items(): # Skip disabled crimes if not data.get("enabled", True): continue # Get button color based on risk style = discord.ButtonStyle.danger if data["risk"] == "high" else discord.ButtonStyle.primary if data["risk"] == "medium" else discord.ButtonStyle.success # Get crime emoji if crime_type == "pickpocket": crime_emoji = "๐Ÿงค" elif crime_type == "mugging": crime_emoji = "๐Ÿ”ช" elif crime_type == "rob_store": crime_emoji = "๐Ÿช" elif crime_type == "random": crime_emoji = "๐ŸŽฒ" else: # bank heist crime_emoji = "๐Ÿ›" # Create button with proper name formatting button = CrimeButton( style=style, label=crime_type.replace('_', ' ').title(), emoji=crime_emoji, custom_id=crime_type, disabled=False # Initially enable all buttons ) self.add_item(button) async def update_button_states(self): """Update button states based on jail and cooldowns""" is_jailed = await self.cog.is_jailed(self.ctx.author) for item in self.children: if isinstance(item, CrimeButton): remaining = await self.cog.get_remaining_cooldown(self.ctx.author, item.custom_id) item.disabled = is_jailed or remaining > 0 if self.message: await self.message.edit(view=self) async def interaction_check(self, interaction: discord.Interaction) -> bool: """Only allow the author that invoked the command to use the interaction""" return interaction.user.id == self.ctx.author.id async def on_timeout(self) -> None: """Handle view timeout""" try: for item in self.children: item.disabled = True if self.message: await self.message.delete() except (discord.NotFound, discord.HTTPException): pass class CrimeView(discord.ui.View): """View for crime confirmation.""" def __init__(self, cog, interaction: discord.Interaction, crime_type: str, crime_data: dict, target: Optional[discord.Member] = None): super().__init__(timeout=30) self.cog = cog self.interaction = interaction self.crime_type = crime_type self.crime_data = crime_data self.target = target self.message = None self.all_messages = [] # Track all messages self.scenario = None self.reward_calculations = [] # Add this line to track reward calculations async def format_crime_message(self, success: bool, is_attempt: bool = False, **kwargs): """Format crime result message.""" currency = await bank.get_currency_name(self.interaction.guild) if is_attempt and self.crime_type == "random": embed = discord.Embed( title="๐ŸŽฒ Random Crime", description="Are you feeling lucky?", color=discord.Color.greyple() ) embed.add_field(name="๐Ÿ“Š Success Rate", value="???", inline=True) embed.add_field(name="๐Ÿ’ธ Potential Fine", value="???", inline=True) return embed if success: embed = discord.Embed( title=f"{self.crime_data.get('emoji', '๐Ÿ’ฐ')} Successful {self.crime_type.replace('_', ' ').title()}!", color=discord.Color.green() ) # Set description based on crime type if self.crime_type == "random": embed.description = _(self.scenario["success_text"]).format( user=self.interaction.user.mention, amount=kwargs.get("reward", 0), currency=currency ) elif self.target: if self.crime_type == "pickpocket": embed.description = f"๐Ÿงค {self.interaction.user.mention} successfully pickpocketed {self.target.mention}!" elif self.crime_type == "mugging": embed.description = f"๐Ÿ”ช {self.interaction.user.mention} successfully mugged {self.target.mention}!" else: if self.crime_type == "pickpocket": embed.description = f"๐Ÿงค {self.interaction.user.mention} successfully picked a pocket!" elif self.crime_type == "mugging": embed.description = f"๐Ÿ”ช {self.interaction.user.mention} successfully mugged someone!" elif self.crime_type == "rob_store": embed.description = f"๐Ÿช {self.interaction.user.mention} successfully robbed the store!" else: # bank heist embed.description = f"๐Ÿ› {self.interaction.user.mention} successfully pulled off a bank heist!" # Add reward calculation breakdown if available if self.reward_calculations: breakdown = [] base_entry = self.reward_calculations[0] # First entry is always base amount # Only add base amount if there are modifiers if len(self.reward_calculations) > 1: breakdown.append(f"Base: {base_entry[1]:,} {currency}") else: # If no modifiers, just show the final amount breakdown.append(f"** {base_entry[1]:,} {currency}**") # Add subsequent calculations for calc in self.reward_calculations[1:]: text, amount, modifier = calc if "streak" in text.lower(): # Explicitly check for streak bonus breakdown.append(f"โžœ {text}: {amount:,} {currency}") elif isinstance(modifier, float): # For other multipliers breakdown.append(f"โžœ ({modifier:.1f}x): {amount:,} {currency}") # Add direct credit changes before final amount if kwargs.get('credit_changes', 0) != 0: credit_change = kwargs['credit_changes'] current_amount = kwargs.get('reward', 0) final_amount = current_amount + credit_change if credit_change > 0: breakdown.append(f"โžœ (+{credit_change:,}): {final_amount:,} {currency}") else: breakdown.append(f"โžœ ({credit_change:,}): {final_amount:,} {currency}") if len(self.reward_calculations) > 1: final_amount = kwargs.get('reward', 0) + kwargs.get('credit_changes', 0) breakdown.append(f"**Final: {final_amount:,} {currency}**") embed.add_field( name="๐Ÿ’ฐ Reward Calculation", value="\n".join(breakdown), inline=False) embed.add_field( name="๐Ÿ“Š Success Rate", value=f"{kwargs.get('rate', int(self.crime_data['success_rate'] * 100))}%", inline=True) return embed else: embed = discord.Embed( title=f"๐Ÿ‘ฎ Failed {self.crime_type.replace('_', ' ').title()}!", color=discord.Color.red() ) if self.crime_type == "random": embed.description = _(self.scenario["fail_text"]).format( user=self.interaction.user.mention, fine=kwargs["fine"], currency=currency ) else: if self.crime_type == "pickpocket": if self.target: embed.description = f"{self.interaction.user.mention} was caught trying to pickpocket {self.target.mention}!" else: embed.description = f"{self.interaction.user.mention} was caught with their hand in someone's pocket!" elif self.crime_type == "mugging": if self.target: embed.description = f"{self.interaction.user.mention} was caught trying to mug {self.target.mention}!" else: embed.description = f"{self.interaction.user.mention} was caught trying to mug someone!" elif self.crime_type == "rob_store": embed.description = f"{self.interaction.user.mention} was caught trying to rob the store!" else: # bank heist embed.description = f"{self.interaction.user.mention} was caught trying to rob the bank!" # Add fine field if kwargs.get("fine", 0) > 0: embed.add_field( name="๐Ÿ’ธ Fine", value=f"{kwargs['fine']:,} {currency}", inline=True ) # Add jail time field if kwargs.get("jail_time", 0) > 0: # Check if user has reduced sentence perk member_data = await self.cog.config.member(self.interaction.user).all() has_reducer = "jail_reducer" in member_data.get("purchased_perks", []) # Check if the jail time might have been doubled (fine not paid) # We determine this by comparing the passed jail_time with the base jail_time for the crime # We need to fetch the base jail time from config or crime_data crime_options = await self.cog.config.guild(self.interaction.guild).crime_options() base_jail_time = crime_options.get(self.crime_type, {}).get("jail_time", 0) # Get base jail time # We also need the event modifier multiplier if applicable jail_multiplier = kwargs.get('jail_multiplier', 1.0) # Calculate the expected base jail time considering event modifiers expected_base_jail_time = int(base_jail_time * jail_multiplier) # Check if the current jail time is double the expected base time (indicates fine penalty) is_doubled = kwargs.get("jail_time", 0) == expected_base_jail_time * 2 if is_doubled: # Calculate original time (before doubling) original_time = expected_base_jail_time doubled_time = kwargs["jail_time"] # Check for reducer perk on the *original* time before doubling if has_reducer: original_time_reduced = int(original_time * 0.8) doubled_time_final = int(original_time_reduced * 2) # Apply doubling AFTER reduction jail_text = f"~~{format_cooldown_time(original_time, include_emoji=False)}~~ โ†’ ~~{format_cooldown_time(original_time_reduced, include_emoji=False)} (-20%)~~ โ†’ {format_cooldown_time(doubled_time_final, include_emoji=False)} (+100%)" else: jail_text = f"~~{format_cooldown_time(original_time, include_emoji=False)}~~ โ†’ {format_cooldown_time(doubled_time, include_emoji=False)} (+100%)" elif has_reducer: # If not doubled, but has reducer, apply reduction as before reduced_time = int(kwargs["jail_time"] * 0.8) # 20% reduction jail_text = f"~~{format_cooldown_time(kwargs['jail_time'], include_emoji=False)}~~ โ†’ {format_cooldown_time(reduced_time, include_emoji=False)} (-20%)" else: # Normal jail time, no doubling or reduction jail_text = format_cooldown_time(kwargs["jail_time"], include_emoji=False) embed.add_field( name="โ›“๏ธ Jail Time", value=jail_text, inline=True ) embed.add_field( name="๐Ÿ“Š Success Rate", value=f"{kwargs.get('rate', int(self.crime_data['success_rate'] * 100))}%", inline=True) return embed async def interaction_check(self, interaction: discord.Interaction) -> bool: """Only allow the original user to use the view.""" return interaction.user.id == self.interaction.user.id async def on_error(self, interaction: discord.Interaction, error: Exception, item: discord.ui.Item): """Handle errors in view interactions.""" await interaction.channel.send( _("An error occurred while processing your crime. Please try again. Error: {error}").format( error=str(error) )) self.stop() async def on_timeout(self): """Handle view timeout""" try: # Disable all buttons for item in self.children: item.disabled = True # Try to update the message if it still exists if self.message: try: await self.message.edit(view=self) if not self.is_finished(): try: msg = await self.message.channel.send(_("Crime timed out.")) self.all_messages.append(msg) except discord.HTTPException: pass except (discord.NotFound, discord.HTTPException): pass except Exception: # Silently handle any other errors since the channel might not be available pass finally: self.stop() @discord.ui.button(label="Confirm", style=discord.ButtonStyle.success) async def confirm(self, interaction: discord.Interaction, button: discord.ui.Button): """Handle crime confirmation""" if interaction.user.bot: return try: await interaction.response.defer() # Get settings settings = await self.cog.config.guild(interaction.guild).global_settings() # Double check cooldown remaining = await self.cog.get_remaining_cooldown(interaction.user, self.crime_type) if remaining > 0: msg = await interaction.channel.send( _("โณ You must wait {hours}h {minutes}m before attempting {crime_type} again!").format( hours=remaining // 3600, minutes=(remaining % 3600) // 60, crime_type=self.crime_type ) ) self.all_messages.append(msg) return # Check if user is jailed if await self.cog.is_jailed(interaction.user): remaining = await self.cog.get_jail_time_remaining(interaction.user) msg = await interaction.channel.send( _("โ›“๏ธ You're still in jail for {minutes}m {seconds}s! You can pay bail using `!crime bail` or jailbreak using `!crime jailbreak`").format( minutes=remaining // 60, seconds=remaining % 60 ) ) self.all_messages.append(msg) return # For targeted crimes, check target's balance before attempting if self.target: try: target_balance = await bank.get_balance(self.target) min_required = max(settings.get("min_steal_balance", 100), self.crime_data["min_reward"]) if target_balance < min_required: msg = await interaction.channel.send( _("Your target doesn't have enough {currency} to steal from! (Minimum: {min:,})").format( currency=await bank.get_currency_name(interaction.guild), min=min_required ) ) self.all_messages.append(msg) return except Exception as e: await interaction.channel.send( _("An error occurred while checking your target's balance. Please try again. Error: {error}").format( error=str(e) ) ) self.all_messages.append(msg) return # Delete confirmation message and target selection message if it exists try: await self.message.delete() # Delete any previous messages (like target selection) for msg in self.all_messages: try: await msg.delete() except (discord.NotFound, discord.HTTPException): pass except (discord.NotFound, discord.HTTPException): pass # Handle random scenario if crime type is random if self.crime_type == "random": scenarios = await get_all_scenarios(self.cog.config, interaction.guild) self.scenario = get_random_scenario(scenarios) self.crime_data = self.crime_data.copy() # Create a copy to modify self.crime_data.update({ "min_reward": self.scenario["min_reward"], "max_reward": self.scenario["max_reward"], "success_rate": self.scenario["success_rate"], "jail_time": self.scenario["jail_time"], "risk": self.scenario["risk"], "fine_multiplier": self.scenario["fine_multiplier"] }) events = [] # Initialize empty events list for random crimes else: # Get and process events if this is not a random crime events = get_crime_event(self.crime_type) # Get attempt message based on crime type if self.crime_type == "random": attempt_msg = _(self.scenario["attempt_text"]).format( user=self.interaction.user.mention ) elif self.crime_type == "pickpocket": attempt_msg = _("๐Ÿงค {user} begins to slip their hand towards {target}'s pocket...").format( user=self.interaction.user.mention, target=self.target.display_name ) elif self.crime_type == "mugging": attempt_msg = _("๐Ÿ”ช {user} lurks in the shadows, waiting for {target}...").format( user=self.interaction.user.mention, target=self.target.display_name ) elif self.crime_type == "rob_store": attempt_msg = _("๐Ÿช {user} pulls out their weapon and approaches the store...").format( user=self.interaction.user.mention ) else: # bank heist attempt_msg = _("๐Ÿ› {user} begins their elaborate plan to breach the bank vault...").format( user=self.interaction.user.mention ) # Create and send attempt message with bail out button attempt_view = CrimeAttemptView(self.cog, interaction, self.crime_type) msg = await interaction.channel.send(attempt_msg, view=attempt_view) attempt_view.message = msg self.all_messages.append(msg) # Short pause after attempt message await asyncio.sleep(2) # Check if user bailed out if attempt_view.bailed: return # Initialize modifiers success_chance = self.crime_data["success_rate"] jail_time = self.crime_data["jail_time"] reward_multiplier = 1.0 cumulative_jail_multiplier = 1.0 # Track jail multiplier from events total_credit_changes = 0 # Track direct credit changes # Get and process events if this is not a random crime if self.crime_type != "random": # Process each event for event in events: # Check for bail out after each event if attempt_view.bailed: return # Send event message with delay event_text = event["text"] format_args = {} # Add credit amounts if present if "credits_bonus" in event: format_args["credits_bonus"] = str(event["credits_bonus"]) elif "credits_penalty" in event: format_args["credits_penalty"] = str(event["credits_penalty"]) # Format the message with all arguments at once msg = await interaction.channel.send(await format_text(event_text, interaction, **format_args)) self.all_messages.append(msg) await asyncio.sleep(4.0) # Increased delay between events # Apply event modifiers if "chance_bonus" in event: success_chance = min(1.0, success_chance + event["chance_bonus"]) elif "chance_penalty" in event: success_chance = max(0.05, success_chance - event["chance_penalty"]) if "reward_multiplier" in event: reward_multiplier *= event["reward_multiplier"] if "jail_multiplier" in event: jail_time = int(jail_time * event["jail_multiplier"]) cumulative_jail_multiplier *= event["jail_multiplier"] # Update cumulative multiplier # Handle direct credit changes (just track the totals here) if "credits_bonus" in event: bonus = event["credits_bonus"] total_credit_changes += bonus elif "credits_penalty" in event: penalty = event["credits_penalty"] total_credit_changes -= penalty # Just track the penalty # Add suspense delay based on risk level if self.crime_data["risk"] == "high": await asyncio.sleep(6) # More suspense for high-risk crimes (was 6s) elif self.crime_data["risk"] == "medium": await asyncio.sleep(5) # Medium delay for medium-risk (was 5s) else: await asyncio.sleep(4) # Quick result for low-risk (was 4s) # Final bail out check before result if attempt_view.bailed: return # Clean up attempt view attempt_view.stop() # Roll for success success = random.random() < success_chance if success: # Handle success if self.target: # For targeted crimes self.crime_data["crime_type"] = self.crime_type # Add crime type to data try: # Calculate base amount base_amount = await calculate_stolen_amount(self.target, self.crime_data, settings) self.reward_calculations = [("Base Amount", base_amount)] current_amount = base_amount # Apply streak bonus if any streak, streak_multiplier = await update_streak(self.cog.config, interaction.user, True) if streak > 0: current_amount = round(current_amount * streak_multiplier) # Round after streak multiplier self.reward_calculations.append((format_streak_text(streak, streak_multiplier), current_amount, streak_multiplier)) # Process reward multipliers from events (excluding direct credits) for event in events: if "reward_multiplier" in event: current_amount = round(current_amount * event["reward_multiplier"]) # Round after each multiplier self.reward_calculations.append((event["text"], current_amount, event["reward_multiplier"])) # Direct credit changes are handled by total_credit_changes # This is the amount BEFORE direct +/- credits from events reward_before_direct_credits = current_amount # Calculate the final amount to transfer final_transfer_amount = reward_before_direct_credits + total_credit_changes # Check target's balance and perform transfer atomically try: target_balance = await bank.get_balance(self.target) min_required = max(settings.get("min_steal_balance", 100), self.crime_data["min_reward"]) if target_balance < min_required: msg = await interaction.channel.send( _("Your target doesn't have enough {currency} to steal from! (Minimum: {min:,})").format( currency=await bank.get_currency_name(interaction.guild), min=min_required ) ) self.all_messages.append(msg) return # Ensure we don't try to take more than the target has or less than zero final_transfer_amount = max(0, min(final_transfer_amount, target_balance)) # Try to perform the transfers await bank.withdraw_credits(self.target, final_transfer_amount) await bank.deposit_credits(interaction.user, final_transfer_amount) # Update stats and last target using the actual amount transferred async with self.cog.config.member(interaction.user).all() as user_data: user_data["total_stolen_from"] += final_transfer_amount user_data["total_credits_earned"] += final_transfer_amount user_data["last_target"] = self.target.id user_data["total_successful_crimes"] += 1 if final_transfer_amount > user_data.get("largest_heist", 0): user_data["largest_heist"] = final_transfer_amount async with self.cog.config.member(self.target).all() as target_data: target_data["total_stolen_by"] += final_transfer_amount # Send success message msg = await interaction.channel.send( embed=await self.format_crime_message( True, target=self.target, reward=reward_before_direct_credits, # Pass amount before direct +/- rate=int(success_chance * 100), settings=settings, credit_changes=total_credit_changes, # Pass the net +/- amount jail_multiplier=cumulative_jail_multiplier # Pass the correct multiplier ) ) self.all_messages.append(msg) for item in attempt_view.children: item.disabled = True await attempt_view.message.edit(view=attempt_view) self.stop() # Stop the view after success except discord.HTTPException as e: await interaction.channel.send( _("Failed to steal from target - they may not have enough {currency}. Error: {error}").format( currency=await bank.get_currency_name(interaction.guild), error=str(e) ) ) self.all_messages.append(msg) return except Exception as e: await interaction.channel.send( _("An error occurred while processing the crime. Please try again. Error: {error}").format( error=str(e) ) ) self.all_messages.append(msg) return else: try: # For non-targeted crimes base_amount = random.randint(self.crime_data["min_reward"], self.crime_data["max_reward"]) self.reward_calculations = [("Base Amount", base_amount)] current_amount = base_amount # Apply streak bonus if any streak, streak_multiplier = await update_streak(self.cog.config, interaction.user, True) if streak > 0: current_amount = round(current_amount * streak_multiplier) # Round after streak multiplier self.reward_calculations.append((format_streak_text(streak, streak_multiplier), current_amount, streak_multiplier)) # Process reward multipliers from events (excluding direct credits) for event in events: if "reward_multiplier" in event: current_amount = round(current_amount * event["reward_multiplier"]) # Round after each multiplier self.reward_calculations.append((event["text"], current_amount, event["reward_multiplier"])) # Direct credit changes are handled by total_credit_changes # This is the amount BEFORE direct +/- credits from events reward_before_direct_credits = current_amount # Calculate the final amount to deposit final_deposit_amount = reward_before_direct_credits + total_credit_changes # Ensure final amount isn't negative after penalties if final_deposit_amount < 0: final_deposit_amount = 0 await bank.deposit_credits(interaction.user, final_deposit_amount) # Send success message msg = await interaction.channel.send( embed=await self.format_crime_message( True, reward=reward_before_direct_credits, # Pass amount before direct +/- rate=int(success_chance * 100), settings=settings, credit_changes=total_credit_changes, # Pass the net +/- amount jail_multiplier=cumulative_jail_multiplier # Pass the correct multiplier ) ) self.all_messages.append(msg) for item in attempt_view.children: item.disabled = True await attempt_view.message.edit(view=attempt_view) self.stop() # Stop the view after success # Update stats using the actual amount deposited async with self.cog.config.member(interaction.user).all() as user_data: user_data["total_credits_earned"] += final_deposit_amount user_data["total_successful_crimes"] += 1 if final_deposit_amount > user_data.get("largest_heist", 0): user_data["largest_heist"] = final_deposit_amount except Exception as e: await interaction.channel.send( _("An error occurred while processing your crime. Please try again. Error: {error}").format( error=str(e) ) ) self.all_messages.append(msg) return else: # Crime failed, processing penalties # Reset streak on failure await update_streak(self.cog.config, interaction.user, False) fine_amount = int(self.crime_data["max_reward"] * self.crime_data["fine_multiplier"]) actual_fine = 0 # Track how much was actually paid # Apply fine if user can afford it try: user_balance = await bank.get_balance(interaction.user) if user_balance >= fine_amount: await bank.withdraw_credits(interaction.user, fine_amount) actual_fine = fine_amount async with self.cog.config.member(interaction.user).all() as user_data: user_data["total_fines_paid"] += fine_amount else: # Take all their money and double jail time if user_balance > 0: # Only take money if they have any await bank.withdraw_credits(interaction.user, user_balance) actual_fine = user_balance async with self.cog.config.member(interaction.user).all() as user_data: user_data["total_fines_paid"] += user_balance # Double the jail time jail_time *= 2 await interaction.channel.send( _("You cannot afford the fine of {fine:,} {currency}. All your money has been confiscated and your jail time has been doubled!").format( fine=fine_amount, currency=await bank.get_currency_name(interaction.guild) ) ) except Exception as e: await interaction.channel.send( _("Failed to apply fine. Error: {error}").format( error=str(e) ) ) self.all_messages.append(msg) return # Send failure message with jail options msg = await interaction.channel.send( embed=await self.format_crime_message( False, fine=actual_fine, jail_time=jail_time, rate=int(success_chance * 100), settings=settings, credit_changes=total_credit_changes, jail_multiplier=cumulative_jail_multiplier # Pass the correct multiplier ) ) self.all_messages.append(msg) # Add jail options view jail_view = JailOptionsView(self.cog, interaction, jail_time) jail_msg = await interaction.channel.send(view=jail_view) jail_view.message = jail_msg self.all_messages.append(jail_msg) # Disable attempt view buttons for item in attempt_view.children: item.disabled = True await attempt_view.message.edit(view=attempt_view) self.stop() # Stop the view after failure # Update stats async with self.cog.config.member(interaction.user).all() as user_data: user_data["total_failed_crimes"] += 1 # Send to jail await self.cog.send_to_jail(interaction.user, jail_time) # Set cooldown await self.cog.set_action_cooldown(interaction.user, self.crime_type) except Exception as e: await interaction.channel.send( _("An error occurred while processing your crime. Please try again. Error: {error}").format( error=str(e) ) ) @discord.ui.button(label="Cancel", style=discord.ButtonStyle.secondary) async def cancel(self, interaction: discord.Interaction, button: discord.ui.Button): """Cancel the crime attempt""" if interaction.user.bot: return try: # Delete all messages including target selection for msg in self.all_messages: try: await msg.delete() except (discord.NotFound, discord.HTTPException): pass # Delete confirmation message try: await self.message.delete() except (discord.NotFound, discord.HTTPException): pass # Send cancellation message msg = await interaction.channel.send(_("Crime cancelled.")) self.stop() except Exception as e: await interaction.channel.send( _("An error occurred while cancelling the crime. Error: {error}").format( error=str(e) ) ) self.stop() class CrimeAttemptView(discord.ui.View): """View for the crime attempt message with Bail Out button.""" def __init__(self, cog, interaction: discord.Interaction, crime_type: str): super().__init__(timeout=30) self.cog = cog self.interaction = interaction self.crime_type = crime_type self.message = None self.bailed = False async def interaction_check(self, interaction: discord.Interaction) -> bool: """Only allow the original user to use the view.""" return interaction.user.id == self.interaction.user.id @discord.ui.button(label="Bail Out!", style=discord.ButtonStyle.danger, emoji="๐Ÿƒ") async def bail_out(self, interaction: discord.Interaction, button: discord.ui.Button): """Handle bailing out of a crime attempt""" if interaction.user.bot: return try: await interaction.response.defer() # Deduct bail out cost currency = await bank.get_currency_name(interaction.guild) try: await bank.withdraw_credits(interaction.user, 100) except ValueError: await interaction.followup.send( _("You don't have enough {currency} to bail out! (Cost: 100)").format( currency=currency ), ephemeral=True ) return # Set cooldown await self.cog.set_action_cooldown(interaction.user, self.crime_type) # Disable the button button.disabled = True await self.message.edit(view=self) # Send bail out message embed = discord.Embed( title="๐Ÿƒ Bailed Out!", description=f"{interaction.user.mention} chickened out and bailed on the {self.crime_type.replace('_', ' ')}!", color=discord.Color.yellow() ) embed.add_field( name="Cost", value=f"100 {currency}", inline=False ) await interaction.followup.send(embed=embed) # Set bailed flag self.bailed = True # Stop the view self.stop() except Exception as e: await interaction.followup.send( _("An error occurred while bailing out. Error: {error}").format( error=str(e) ), ephemeral=True ) self.stop() async def on_timeout(self) -> None: """Handle view timeout""" if self.bailed: # If already bailed out, don't try to modify the message return try: # Disable the button for item in self.children: item.disabled = True # Try to update the message if it still exists if self.message: try: await self.message.edit(view=self) except (discord.NotFound, discord.HTTPException): # Message was deleted or became invalid, just ignore pass except Exception as e: # Log any other unexpected errors but don't try to send them # since the channel/message might not be available pass class BailView(discord.ui.View): """View for paying bail""" def __init__(self, cog, ctx: commands.Context, bail_amount: int, jail_time: int): super().__init__(timeout=30) self.cog = cog self.ctx = ctx self.bail_amount = bail_amount self.jail_time = jail_time self.message = None # Will store the initial bail prompt message self.all_messages = [] # Track all messages def format_bail_embed(self, title: str, description: str, color: discord.Color = discord.Color.blue()) -> discord.Embed: """Format a bail-related embed with consistent styling.""" embed = discord.Embed( title=title, description=description, color=color, timestamp=discord.utils.utcnow() ) embed.set_footer(text=f"Requested by {self.ctx.author.display_name}", icon_url=self.ctx.author.display_avatar.url) return embed async def cleanup_messages(self): """Clean up all messages sent during the bail process""" try: # Always add the initial bail prompt message to cleanup list if self.message: self.all_messages.append(self.message) for msg in self.all_messages: try: await msg.delete() except (discord.NotFound, discord.Forbidden): pass except Exception as e: error_embed = self.format_bail_embed( "โš ๏ธ Error", f"An error occurred while cleaning up messages: {str(e)}", discord.Color.red() ) await self.message.channel.send(embed=error_embed) async def interaction_check(self, interaction: discord.Interaction) -> bool: """Only allow the author that invoked the command to use the interaction""" return interaction.user.id == self.ctx.author.id async def on_error(self, interaction: discord.Interaction, error: Exception, item: discord.ui.Item): """Handle any errors that occur during button interactions""" msg = await interaction.channel.send( _("An error occurred. Please try again. Error: {error}").format( error=str(error) ) ) self.all_messages.append(msg) await self.cleanup_messages() self.stop() @discord.ui.button(label="Pay Bail", style=discord.ButtonStyle.success, emoji="๐Ÿ’ธ") async def pay_bail(self, interaction: discord.Interaction, button: discord.ui.Button): """Pay bail and get out of jail""" if interaction.user.bot: return try: # Get current balance and currency name current_balance = await bank.get_balance(interaction.user) currency_name = await bank.get_currency_name(interaction.guild) # Check if user has enough credits if not await bank.can_spend(interaction.user, self.bail_amount): insufficient_embed = self.format_bail_embed( "๐Ÿ’ต Insufficient Funds", f"You don't have enough {currency_name} to pay bail!\n\n" f"**Required:** {self.bail_amount:,} {currency_name}\n" f"**Current Balance:** {current_balance:,} {currency_name}", discord.Color.red() ) msg = await interaction.channel.send(embed=insufficient_embed) self.all_messages.append(msg) return # Pay bail and remove from jail await bank.withdraw_credits(interaction.user, self.bail_amount) # Get new balance new_balance = await bank.get_balance(interaction.user) # Update jail status and stats async with self.cog.config.member(interaction.user).all() as user_data: user_data["jail_until"] = 0 user_data["total_bail_paid"] = user_data.get("total_bail_paid", 0) + self.bail_amount # Cancel any pending release notification await self.cog._cancel_notification(interaction.user) # Clean up the bail prompt first await self.cleanup_messages() # Send success message (this one stays) success_embed = self.format_bail_embed( "๐Ÿ”“ Bail Paid Successfully!", f"You have been released from jail.\n\n" f"**Bail Cost:** {self.bail_amount:,} {currency_name}\n" f"**Previous Balance:** {current_balance:,} {currency_name}\n" f"**New Balance:** {new_balance:,} {currency_name}", discord.Color.green() ) await interaction.channel.send(embed=success_embed) self.stop() except Exception as e: error_embed = self.format_bail_embed( "โš ๏ธ Error", f"An error occurred while paying bail: {str(e)}", discord.Color.red() ) msg = await interaction.channel.send(embed=error_embed) self.all_messages.append(msg) await self.cleanup_messages() self.stop() @discord.ui.button(label="Cancel", style=discord.ButtonStyle.danger, emoji="โŒ") async def cancel(self, interaction: discord.Interaction, button: discord.ui.Button): """Cancel bail payment""" if interaction.user.bot: return try: minutes = self.jail_time // 60 seconds = self.jail_time % 60 # Check if user has reduced sentence perk member_data = await self.cog.config.member(interaction.user).all() has_reducer = "jail_reducer" in member_data.get("purchased_perks", []) time_text = f"{minutes}m {seconds}s" if has_reducer: time_text += " (Reduced by 20%)" cancel_embed = self.format_bail_embed( "โŒ Bail Cancelled", f"You have chosen to serve your time.\n\n" f"**Time Remaining:** {time_text}", discord.Color.orange() ) msg = await interaction.channel.send(embed=cancel_embed) self.all_messages.append(msg) await self.cleanup_messages() self.stop() except Exception as e: error_embed = self.format_bail_embed( "โš ๏ธ Error", f"An error occurred while cancelling bail: {str(e)}", discord.Color.red() ) await interaction.channel.send(embed=error_embed) async def on_timeout(self): """Handle view timeout""" try: for item in self.children: item.disabled = True if self.message: await self.message.edit(view=self) timeout_embed = self.format_bail_embed( "โฐ Time's Up", "Bail payment timed out.", discord.Color.greyple() ) msg = await self.message.channel.send(embed=timeout_embed) self.all_messages.append(msg) await self.cleanup_messages() except Exception: # Silently handle any other errors since the channel might not be available pass finally: self.stop() class TargetModal(discord.ui.Modal): """Modal for entering target information""" def __init__(self, view): super().__init__(title="Select Target") self.view = view self.target_input = discord.ui.TextInput( label="Target User", placeholder="Enter username, nickname, or ID", required=True, min_length=1, max_length=100 ) self.add_item(self.target_input) async def on_submit(self, interaction: discord.Interaction): """Handle target selection submission""" try: await interaction.response.defer() # Try to find the target member exact_matches = [] partial_matches = [] input_value = self.target_input.value.lower() # First collect all exact and partial matches for member in interaction.guild.members: # Check exact matches first if (input_value == member.name.lower() or input_value == member.display_name.lower() or input_value == str(member.id)): exact_matches.append(member) elif (input_value in member.name.lower() or input_value in member.display_name.lower()): partial_matches.append(member) # Handle multiple exact matches if len(exact_matches) > 1: # Format the list of exact matches with their details match_list = [] for i, member in enumerate(exact_matches, 1): if member.nick: match_list.append(f"{i}. @{member.name} (Nickname: {member.nick})") else: match_list.append(f"{i}. @{member.name}") msg = await interaction.followup.send( _("Multiple users found with that exact name/nickname:\n```\n{}\n```\n" "Please use their Discord ID or full @username to target a specific user.").format( '\n'.join(match_list) ) ) self.view.all_messages.append(msg) return elif len(exact_matches) == 1: target = exact_matches[0] # Handle partial matches only if no exact matches elif partial_matches: # Format the list of partial matches with their details match_list = [] for i, member in enumerate(partial_matches, 1): if member.nick: match_list.append(f"{i}. @{member.name} (Nickname: {member.nick})") else: match_list.append(f"{i}. @{member.name}") msg = await interaction.followup.send( _("Multiple possible matches found:\n```\n{}\n```\n" "Please be more specific or use their Discord ID or full @username.").format( '\n'.join(match_list[:10]) # Limit to first 10 matches ) ) self.view.all_messages.append(msg) return else: msg = await interaction.followup.send( _("Could not find a member named '{name}'. Please check the spelling and try again.").format( name=self.target_input.value ) ) self.view.all_messages.append(msg) return # Check if target is valid settings = await self.view.cog.config.guild(interaction.guild).global_settings() can_target, reason = await can_target_for_crime(self.view.cog, interaction, target, self.view.crime_data, settings) if not can_target: msg = await interaction.followup.send(reason) self.view.all_messages.append(msg) return # Check target's balance before proceeding try: target_balance = await bank.get_balance(target) min_required = max(settings.get("min_steal_balance", 100), self.view.crime_data["min_reward"]) if target_balance < min_required: msg = await interaction.followup.send( _("This target doesn't have enough {currency} to steal from! (Minimum: {min:,})").format( currency=await bank.get_currency_name(interaction.guild), min=min_required ) ) self.view.all_messages.append(msg) return except Exception as e: await interaction.followup.send( _("An error occurred while checking your target's balance. Please try again. Error: {error}").format( error=str(e) ) ) return # Create crime view with selected target crime_view = CrimeView(self.view.cog, interaction, self.view.crime_type, self.view.crime_data, target=target) embed = discord.Embed( title=f"{self.view.crime_data.get('emoji', '๐ŸŽฏ')} Target Selected", description=f"Ready to attempt {self.view.crime_type.replace('_', ' ')} against {target.display_name}?", color=discord.Color.red() ) embed.add_field( name="๐Ÿ“Š Success Rate", value=f"{int(self.view.crime_data['success_rate'] * 100)}%", inline=True ) embed.add_field( name="๐Ÿ’ธ Potential Fine", value=f"{int(self.view.crime_data['max_reward'] * self.view.crime_data['fine_multiplier']):,} {await bank.get_currency_name(interaction.guild)}", inline=True ) # Add target details field for clarity target_details = f"Username: @{target.name}" target_details += f"\nBank Balance: {target_balance:,} {await bank.get_currency_name(interaction.guild)}" embed.add_field( name="๐ŸŽฏ Target Details", value=target_details, inline=False ) message = await interaction.followup.send( embed=embed, view=crime_view ) crime_view.message = message crime_view.all_messages = self.view.all_messages + [message] # Pass message list to crime view # Stop the target selection view self.view.stop() except Exception as e: await interaction.followup.send( _("An error occurred while selecting the target. Please try again. Error: {error}").format( error=str(e) ) ) class TargetSelectionView(discord.ui.View): """View for selecting a target""" def __init__(self, cog, interaction: discord.Interaction, crime_type: str, crime_data: dict): super().__init__(timeout=60) self.cog = cog self.interaction = interaction self.crime_type = crime_type self.crime_data = crime_data self.target = None self.message = None self.all_messages = [] # Track all messages async def cleanup_messages(self): """Clean up all messages sent during the crime process""" try: for msg in self.all_messages: try: await msg.delete() except (discord.NotFound, discord.Forbidden): pass except Exception as e: await self.message.channel.send( _("An error occurred while cleaning up messages. Error: {error}").format( error=str(e) ) ) async def get_random_target(self) -> Optional[discord.Member]: """Get a random valid target from the guild.""" try: # Get settings first - we need this for all checks try: settings = await self.cog.config.guild(self.interaction.guild).global_settings() min_required = max(settings.get("min_steal_balance", 100), self.crime_data["min_reward"]) except AttributeError: await self.interaction.channel.send(_("Error: Could not access guild settings. Please try again.")) return None except Exception as e: await self.interaction.channel.send(_("Error: Could not load settings. Error: {error}").format(error=str(e))) return None # Get last target ID once - cheap memory lookup try: last_target_id = await self.cog.config.member(self.interaction.user).last_target() except Exception: last_target_id = None # Initial filtering with optimized memory usage all_members = [] try: for member in self.interaction.guild.members: # Combined early filtering with clear conditions if (member.bot or member.id == self.interaction.user.id or (last_target_id is not None and member.id == last_target_id)): # Skip bots, self, and last target continue all_members.append(member) except AttributeError: await self.interaction.channel.send(_("Error: Could not access guild members. Please try again.")) return None if not all_members: await self.interaction.channel.send(_("No valid targets found! Everyone is either a bot or the only member found was already your last target.")) return None random.shuffle(all_members) # Get list of jailed members once instead of checking individually jailed_members = set() jail_check_errors = 0 for member in all_members: try: if await self.cog.is_jailed(member): jailed_members.add(member.id) except Exception: jail_check_errors += 1 if jail_check_errors > min(5, len(all_members) * 0.1): # 10% or 5 errors, whichever is smaller await self.interaction.channel.send(_("Error: Too many jail status check failures. Please try again.")) return None continue # Process members in chunks chunk_size = min(25, max(10, len(all_members) // 20)) # Dynamic chunk size total_checked = 0 balance_check_errors = 0 targeting_check_errors = 0 # Pre-cache bank data for first chunk to avoid initial lag try: first_chunk = all_members[:chunk_size] balance_tasks = [bank.get_balance(member) for member in first_chunk if member.id not in jailed_members] if balance_tasks: await asyncio.gather(*balance_tasks, return_exceptions=True) except Exception: pass # Ignore pre-cache errors while total_checked < len(all_members): chunk_end = min(total_checked + chunk_size, len(all_members)) current_chunk = all_members[total_checked:chunk_end] # Check balances in parallel for the chunk balance_tasks = [] chunk_members = [] for member in current_chunk: if member.id not in jailed_members: balance_tasks.append(bank.get_balance(member)) chunk_members.append(member) if balance_tasks: try: balance_results = await asyncio.gather(*balance_tasks, return_exceptions=True) for member, balance_result in zip(chunk_members, balance_results): if isinstance(balance_result, Exception): if not isinstance(balance_result, discord.NotFound): balance_check_errors += 1 continue if balance_result >= min_required: try: can_target, reason = await can_target_for_crime(self.cog, self.interaction, member, self.crime_data, settings) if can_target: return member except discord.NotFound: continue except Exception: targeting_check_errors += 1 except Exception: balance_check_errors += len(balance_tasks) # Check error thresholds if balance_check_errors > min(5, len(all_members) * 0.1): await self.interaction.channel.send(_("Error: Multiple balance check failures. Please try again later.")) return None if targeting_check_errors > min(5, len(all_members) * 0.1): await self.interaction.channel.send(_("Error: Multiple targeting check failures. Please try again later.")) return None total_checked += chunk_size # Stop if we've checked enough members if total_checked >= len(all_members) * 0.5: break return None except discord.NotFound: await self.interaction.channel.send(_("Error: The server or channel could not be found. Please try again.")) return None except discord.Forbidden: await self.interaction.channel.send(_("Error: I don't have permission to perform this action.")) return None except Exception as e: await self.interaction.channel.send( _("An unexpected error occurred while finding a random target. Please try again later. Error: {error}") .format(error=str(e)) ) return None async def interaction_check(self, interaction: discord.Interaction) -> bool: """Only allow the author that invoked the command to use the interaction""" return interaction.user == self.interaction.user async def on_error(self, interaction: discord.Interaction, error: Exception, item: discord.ui.Item): """Handle any errors that occur during button interactions""" msg = await interaction.channel.send( _("An error occurred. Please try again. Error: {error}").format( error=str(error) ) ) self.all_messages.append(msg) await self.cleanup_messages() self.stop() @discord.ui.button(label="Random Target", style=discord.ButtonStyle.primary) async def random_target(self, interaction: discord.Interaction, button: discord.ui.Button): """Select a random target""" if interaction.user.bot: return try: await interaction.response.defer() target = await self.get_random_target() if target: # Create crime view with selected target crime_view = CrimeView(self.cog, interaction, self.crime_type, self.crime_data, target=target) embed = discord.Embed( title=f"{self.crime_data.get('emoji', '๐ŸŽฏ')} Target Selected", description=f"Ready to attempt {self.crime_type.replace('_', ' ')} against {target.display_name}?", color=discord.Color.red() ) embed.add_field( name="๐Ÿ“Š Success Rate", value=f"{int(self.crime_data['success_rate'] * 100)}%", inline=True ) embed.add_field( name="๐Ÿ’ธ Potential Fine", value=f"{int(self.crime_data['max_reward'] * self.crime_data['fine_multiplier']):,} {await bank.get_currency_name(interaction.guild)}", inline=True ) message = await interaction.channel.send( embed=embed, view=crime_view ) crime_view.message = message crime_view.all_messages = self.all_messages + [message] # Pass message list to crime view self.stop() else: settings = await self.cog.config.guild(interaction.guild).global_settings() no_target_msg = await interaction.channel.send( _("No valid targets found. A valid target must:\n" "โ€ข Have at least {min_balance:,} {currency}\n" "โ€ข Not be your last target\n" "โ€ข Not be in jail\n" "Try again later or choose a specific target.").format( min_balance=settings.get("min_steal_balance", 100), currency=await bank.get_currency_name(interaction.guild) ) ) self.all_messages.append(no_target_msg) except Exception as e: error_msg = await interaction.channel.send( _("An error occurred while selecting a target. Please try again. Error: {error}").format( error=str(e) ) ) self.all_messages.append(error_msg) @discord.ui.button(label="Select Target", style=discord.ButtonStyle.success) async def select_target(self, interaction: discord.Interaction, button: discord.ui.Button): """Open modal to select specific target""" if interaction.user.bot: return modal = TargetModal(self) await interaction.response.send_modal(modal) @discord.ui.button(label="Cancel", style=discord.ButtonStyle.danger) async def cancel(self, interaction: discord.Interaction, button: discord.ui.Button): """Cancel target selection""" if interaction.user.bot: return await interaction.response.defer() msg = await interaction.channel.send(_("Crime cancelled.")) self.all_messages.append(msg) await self.cleanup_messages() self.target = None self.stop() async def on_timeout(self): """Handle view timeout""" try: # Disable all buttons for item in self.children: item.disabled = True # Try to update the message if it still exists if self.message: try: await self.message.edit(view=self) try: msg = await self.message.channel.send(_("Target selection timed out.")) self.all_messages.append(msg) await self.cleanup_messages() except discord.HTTPException: pass except (discord.NotFound, discord.HTTPException): pass except Exception: # Silently handle any other errors since the channel might not be available pass finally: self.stop() async def can_target_for_crime(cog, interaction: discord.Interaction, target: discord.Member, crime_data: dict, settings: dict) -> Tuple[bool, str]: """Check if a user can be targeted for a crime. Args: cog: The crime cog instance interaction: The discord interaction target: The member to check if can be targeted crime_data: The crime data containing requirements settings: Global settings containing minimum balance requirements Returns: tuple of (can_target, reason) """ # First do basic checks using the utility function # Add min_balance_required flag for crime actions crime_data = {**crime_data, "min_balance_required": True} can_target, reason = await can_target_user(interaction, target, crime_data, settings) if not can_target: return False, reason # Check if target is jailed if await cog.is_jailed(target): return False, _("That user is in jail!") # Check if target was last victim last_target = await cog.config.member(interaction.user).last_target() if last_target is not None and last_target == target.id: return False, _("You can't target your last victim!") return True, "" class MainMenuSelect(discord.ui.Select): """Dropdown select menu for the main crime menu.""" def __init__(self, cog, ctx): self.cog = cog self.ctx = ctx # Create base options list self.base_options = [ discord.SelectOption( label="Commit Crime", value="crime", description="Choose a crime to commit", emoji="๐Ÿฆน" ), discord.SelectOption( label="Pay Bail", value="bail", description="Pay to get out of jail early", emoji="๐Ÿ’ฐ" ), discord.SelectOption( label="Attempt Jailbreak", value="jailbreak", description="Try to escape from jail", emoji="๐Ÿ”“" ), discord.SelectOption( label="Leaderboard", value="leaderboard", description="View the crime leaderboard", emoji="๐Ÿ†" ), discord.SelectOption( label="View Status", value="status", description="Check your criminal status", emoji="โณ" ), discord.SelectOption( label="View Stats", value="stats", description="View your crime statistics", emoji="๐Ÿ“Š" ), discord.SelectOption( label="Inventory", value="inventory", description="View and manage your items", emoji="๐ŸŽ’" ), discord.SelectOption( label="Black Market", value="blackmarket", description="Purchase special items and perks", emoji="๐Ÿดโ€โ˜ ๏ธ" ) ] super().__init__( placeholder="Choose an action...", min_values=1, max_values=1, options=self.base_options ) async def update_options(self): """Update options based on user's current status.""" is_jailed = await self.cog.is_jailed(self.ctx.author) member_data = await self.cog.config.member(self.ctx.author).all() # Create a new options list based on user status options = [] for option in self.base_options: if option.value == "crime" and is_jailed: # Update description for jailed users new_option = discord.SelectOption( label=option.label, value=option.value, description="(Unavailable) Cannot commit crimes while in jail", emoji=option.emoji ) elif option.value in ["bail", "jailbreak"] and not is_jailed: # Update description for non-jailed users new_option = discord.SelectOption( label=option.label, value=option.value, description="(Unavailable) Only available while in jail", emoji=option.emoji ) elif option.value == "jailbreak" and member_data.get("attempted_jailbreak", False): # Update description for failed jailbreak new_option = discord.SelectOption( label=option.label, value=option.value, description="(Unavailable) Already attempted jailbreak this sentence", emoji=option.emoji ) else: new_option = option options.append(new_option) self.options = options # Disable the entire select menu if all options would be disabled self.disabled = is_jailed and all(opt.value in ["crime"] for opt in options) or \ (not is_jailed and all(opt.value in ["bail", "jailbreak"] for opt in options)) # Update the message with new options if self.view and self.view.message: await self.view.message.edit(view=self.view) async def callback(self, interaction: discord.Interaction): """Handle menu selection.""" if interaction.user.id != self.ctx.author.id: await interaction.response.send_message("This menu is not for you!", ephemeral=True) return # Get current jail status is_jailed = await self.cog.is_jailed(self.ctx.author) member_data = await self.cog.config.member(self.ctx.author).all() action = self.values[0] # Validate the selection based on current status if action == "crime" and is_jailed: await interaction.response.send_message("You cannot commit crimes while in jail!", ephemeral=True) return elif action in ["bail", "jailbreak"] and not is_jailed: await interaction.response.send_message("You are not in jail!", ephemeral=True) return elif action == "jailbreak" and member_data.get("attempted_jailbreak", False): await interaction.response.send_message("You've already attempted to break out this sentence!", ephemeral=True) return # Delete the main menu message try: await self.view.message.delete() except (discord.NotFound, discord.HTTPException): pass # Handle different actions if action == "crime": await self.ctx.invoke(self.cog.crime_commit) elif action == "bail": await self.ctx.invoke(self.cog.crime_bail) elif action == "jailbreak": await self.ctx.invoke(self.cog.crime_jailbreak) elif action == "leaderboard": await self.ctx.invoke(self.cog.crime_leaderboard) elif action == "status": await self.ctx.invoke(self.cog.crime_status) elif action == "stats": await self.ctx.invoke(self.cog.crime_stats) elif action == "inventory": await self.ctx.invoke(self.cog.city_inventory) elif action == "blackmarket": await self.ctx.invoke(self.cog.crime_blackmarket) class MainMenuView(discord.ui.View): """View for the main crime menu.""" def __init__(self, cog, ctx): super().__init__(timeout=60) self.cog = cog self.ctx = ctx self.message: Optional[discord.Message] = None # Add select menu self.select_menu = MainMenuSelect(cog, ctx) self.add_item(self.select_menu) async def initialize_menu(self): """Initialize the menu when first shown.""" await self.select_menu.update_options() async def on_timeout(self): """Handle view timeout.""" try: self.select_menu.disabled = True if self.message: await self.message.edit(view=self) except (discord.NotFound, discord.HTTPException): pass class AddScenarioModal(discord.ui.Modal): """Modal for adding a custom random scenario.""" def __init__(self, cog): super().__init__(title="Add Custom Random Scenario") self.cog = cog self.name = discord.ui.TextInput( label="Scenario Name", placeholder="e.g. cookie_heist", required=True, min_length=3, max_length=50 ) self.add_item(self.name) self.risk = discord.ui.TextInput( label="Risk Level", placeholder="low, medium, or high", required=True, min_length=3, max_length=6 ) self.add_item(self.risk) self.attempt_text = discord.ui.TextInput( label="Attempt Text", placeholder="๐Ÿช {user} sneaks into the cookie factory...", required=True, min_length=10, max_length=200 ) self.add_item(self.attempt_text) self.success_text = discord.ui.TextInput( label="Success Text", placeholder="๐Ÿช {user} made off with cookies worth {amount} {currency}!", required=True, min_length=10, max_length=200 ) self.add_item(self.success_text) self.fail_text = discord.ui.TextInput( label="Fail Text", placeholder="๐Ÿช {user} got caught with their hand in the cookie jar!", required=True, min_length=10, max_length=200 ) self.add_item(self.fail_text) async def on_submit(self, interaction: discord.Interaction): """Handle form submission.""" # Validate risk level risk = self.risk.value.lower() if risk not in ["low", "medium", "high"]: await interaction.response.send_message( "Invalid risk level. Must be 'low', 'medium', or 'high'.", ephemeral=True ) return # Get corresponding success rate and other values based on risk if risk == "low": success_rate = SUCCESS_RATE_HIGH min_reward = 100 max_reward = 300 jail_time = 180 fine_multiplier = 0.3 elif risk == "medium": success_rate = SUCCESS_RATE_MEDIUM min_reward = 300 max_reward = 800 jail_time = 300 fine_multiplier = 0.4 else: # high success_rate = SUCCESS_RATE_LOW min_reward = 800 max_reward = 2000 jail_time = 600 fine_multiplier = 0.5 # Create new scenario new_scenario = { "name": self.name.value.lower(), "risk": risk, "min_reward": min_reward, "max_reward": max_reward, "success_rate": success_rate, "jail_time": jail_time, "fine_multiplier": fine_multiplier, "attempt_text": self.attempt_text.value, "success_text": self.success_text.value, "fail_text": self.fail_text.value } # Add to guild's custom scenarios await add_custom_scenario(self.cog.config, interaction.guild, new_scenario) # Send confirmation embed = discord.Embed( title="โœ… Custom Scenario Added!", description=f"Your scenario '{self.name.value}' has been added to this server's random crime pool.", color=discord.Color.green() ) embed.add_field(name="Risk Level", value=risk.title(), inline=True) embed.add_field(name="Success Rate", value=f"{int(success_rate * 100)}%", inline=True) embed.add_field(name="Reward Range", value=f"{min_reward:,} - {max_reward:,}", inline=True) await interaction.response.send_message(embed=embed) class JailOptionsView(discord.ui.View): """View for jail options after a failed crime.""" def __init__(self, cog, interaction: discord.Interaction, jail_time: int): super().__init__(timeout=60) self.cog = cog self.interaction = interaction self.jail_time = jail_time self.message = None async def interaction_check(self, interaction: discord.Interaction) -> bool: """Only allow the original user to use the view.""" return interaction.user.id == self.interaction.user.id async def on_timeout(self) -> None: """Handle view timeout""" try: for item in self.children: item.disabled = True await self.message.edit(view=self) except (discord.NotFound, discord.HTTPException): pass @discord.ui.button(label="Jail Break", style=discord.ButtonStyle.danger, emoji="๐Ÿ”“") async def jailbreak(self, interaction: discord.Interaction, button: discord.ui.Button): """Attempt a jailbreak""" if interaction.user.bot: return try: await interaction.response.defer() # Disable the jailbreak button immediately after deferring button.disabled = True await self.message.edit(view=self) # Create context from interaction ctx = await self.cog.bot.get_context(interaction.message) ctx.author = interaction.user # Use existing jailbreak command await self.cog.crime_jailbreak(ctx) except Exception as e: await interaction.followup.send( _("An error occurred while attempting jailbreak. Error: {error}").format( error=str(e) ), ephemeral=True ) @discord.ui.button(label="Pay Bail", style=discord.ButtonStyle.success, emoji="๐Ÿ’ธ") async def pay_bail(self, interaction: discord.Interaction, button: discord.ui.Button): """Pay bail to get out of jail""" if interaction.user.bot: return try: await interaction.response.defer() # Create context from interaction ctx = await self.cog.bot.get_context(interaction.message) ctx.author = interaction.user # Use existing bail command await self.cog.crime_bail(ctx) # Disable buttons after use for item in self.children: item.disabled = True await self.message.edit(view=self) self.stop() except Exception as e: await interaction.followup.send( _("An error occurred while paying bail. Error: {error}").format( error=str(e) ), ephemeral=True )