Ruby-Cogs/city/crime/views.py

2055 lines
91 KiB
Python

"""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
)