Ruby-Cogs/city/crime/commands.py

1238 lines
54 KiB
Python

"""Commands for the crime system."""
from redbot.core import commands, bank
from redbot.core.utils.chat_formatting import box, humanize_number
from redbot.core.i18n import Translator
import discord
import random
import time
import asyncio
from .scenarios import get_random_scenario, get_random_jailbreak_scenario
from .views import CrimeListView, BailView, CrimeView, TargetSelectionView, CrimeButton, MainMenuView, AddScenarioModal
from .data import CRIME_TYPES, DEFAULT_GUILD, DEFAULT_MEMBER
from datetime import datetime
from ..utils import (
format_cooldown_time,
get_crime_emoji,
format_crime_description,
format_streak_text
)
_ = Translator("Crime", __file__)
class CrimeCommands:
"""Crime commands mixin."""
def __init__(self, bot):
self.bot = bot
self.notification_tasks = {} # Store notification tasks by member ID
@commands.group(name="crime", invoke_without_command=True)
async def crime(self, ctx: commands.Context):
"""Access the crime system.
If no subcommand is provided, shows the main menu with all available actions.
"""
try:
# Get member data
member_data = await self.config.member(ctx.author).all()
# Get jail status
jail_remaining = await self.get_jail_time_remaining(ctx.author)
status = "⛓️ In jail" if jail_remaining > 0 else "✅ Free"
# Get streak info
streak = member_data.get("current_streak", 0)
streak_multiplier = member_data.get("streak_multiplier", 1.0)
streak_text = format_streak_text(streak, streak_multiplier)
# Create embed
embed = discord.Embed(
title="🌃 Welcome to the Criminal Underworld",
description=(
"The city never sleeps, and neither do its criminals. What kind of trouble are you looking to get into today?\n\n"
"Choose your next move wisely..."
),
color=discord.Color.dark_red()
)
# Add criminal record field
embed.add_field(
name="__Your Criminal Record__",
value=(
f"🦹 Current Status: {status}\n"
f"💰 Lifetime Earnings: {humanize_number(member_data['total_credits_earned'])} {await bank.get_currency_name(ctx.guild)}\n"
f"✅ Successful Crimes: {member_data['total_successful_crimes']}\n"
f"❌ Failed Attempts: {member_data['total_failed_crimes']}\n"
f"🏆 Largest Heist: {humanize_number(member_data['largest_heist'])} {await bank.get_currency_name(ctx.guild)}\n"
f"📈 Current Streak: {streak_text}"
),
inline=False
)
# Create and send view
view = MainMenuView(self, ctx)
view.message = await ctx.send(embed=embed, view=view)
# Initialize menu options
await view.initialize_menu()
except Exception as e:
await ctx.send(f"An error occurred while opening the crime menu: {str(e)}")
@crime.command(name="commit")
async def crime_commit(self, ctx: commands.Context):
"""Choose a crime to commit using an interactive menu
Available crimes:
• 🧤 Pickpocket: Low risk, target users for small rewards
• 🔪 Mugging: Medium risk, target users for medium rewards
• 🏪 Store Robbery: Medium risk, no target needed
• 🏛 Bank Heist: High risk, high rewards
• 🎲 Random Crime: Random risk and rewards
Each crime has:
• Color-coded risk levels (green=low, blue=medium, red=high)
• Success rates shown before committing
• Cooldown periods between attempts
• Fines and jail time if caught
Getting caught will send you to jail!
"""
try:
# Get guild settings
settings = await self.config.guild(ctx.guild).global_settings()
crime_options = await self.config.guild(ctx.guild).crime_options()
# Check jail status
jail_remaining = await self.get_jail_time_remaining(ctx.author)
jail_status = ""
if jail_remaining > 0:
if jail_remaining > 3600: # More than 1 hour
hours = jail_remaining // 3600
minutes = (jail_remaining % 3600) // 60
jail_status = f"⛓️ **JAILED** for {hours}h {minutes}m"
else:
minutes = jail_remaining // 60
seconds = jail_remaining % 60
jail_status = f"⛓️ **JAILED** for {minutes}m {seconds}s"
# Create embed with crime options
embed = discord.Embed(
title="🦹‍♂️ Criminal Activities",
description=_(
"Choose your next heist wisely...\n"
"{jail_status}\n"
"**Fines:**\n"
"🟢 Low Risk: 30-35% of max reward\n"
"🟡 Medium Risk: 40-45% of max reward\n"
"🔴 High Risk: 45-50% of max reward\n\n"
).format(
jail_status=jail_status + "\n" if jail_status else ""
),
color=await ctx.embed_color()
)
# Add crime options to embed
crimes = list(crime_options.items())
for i in range(0, len(crimes), 2):
# Get current crime
crime_type, data = crimes[i]
# Get cooldown status
remaining = await self.get_remaining_cooldown(ctx.author, crime_type)
status = format_cooldown_time(remaining)
# Format description
description = format_crime_description(crime_type, data, status)
# Check if there's a next crime to pair with
if i + 1 < len(crimes):
# Get next crime
next_crime_type, next_data = crimes[i + 1]
# Get cooldown status for next crime
next_remaining = await self.get_remaining_cooldown(ctx.author, next_crime_type)
next_status = format_cooldown_time(next_remaining)
# Format next description
next_description = format_crime_description(next_crime_type, next_data, next_status)
# Add both crimes side by side
embed.add_field(
name=f"{get_crime_emoji(crime_type)} __**{crime_type.replace('_', ' ').title()}**__",
value=description,
inline=True
)
embed.add_field(
name=f"{get_crime_emoji(next_crime_type)} __**{next_crime_type.replace('_', ' ').title()}**__",
value=next_description,
inline=True
)
# Add empty field to force next pair to start on new line
embed.add_field(name="\u200b", value="\u200b", inline=True)
else:
# Add single crime if no pair
embed.add_field(
name=f"{get_crime_emoji(crime_type)} __**{crime_type.replace('_', ' ').title()}**__",
value=description,
inline=True
)
embed.set_footer(text="Use the buttons below to choose a crime")
# Create view with crime buttons
view = CrimeListView(self, ctx, crime_options)
message = await ctx.send(embed=embed, view=view)
view.message = message
# Update button states based on jail and cooldowns
await view.update_button_states()
except Exception as e:
await ctx.send(_("An error occurred while setting up the crime options. Please try again. Error: {}").format(str(e)))
@crime.command(name="status")
async def crime_status(self, ctx: commands.Context, user: discord.Member = None):
"""View current jail status, cooldowns, and other active states
Parameters
----------
user : discord.Member, optional
The user to check status for. If not provided, shows your own status."""
try:
# Get member data
target = user or ctx.author
member_data = await self.config.member(target).all()
settings = await self.config.guild(ctx.guild).global_settings()
crime_options = await self.config.guild(ctx.guild).crime_options()
# Create status embed
embed = discord.Embed(
title="🦹‍♂️ Criminal Status",
description=f"Current status for {target.mention}",
color=await ctx.embed_color()
)
# Set thumbnail to user's avatar
embed.set_thumbnail(url=target.display_avatar.url)
# Check jail status
remaining_jail = await self.get_jail_time_remaining(target)
if remaining_jail > 0:
# Check if user has reduced sentence perk
has_reducer = "jail_reducer" in member_data.get("purchased_perks", [])
if has_reducer:
# Calculate original time (current time is after 20% reduction)
original_time = int(remaining_jail / 0.8) # Reverse the 20% reduction
jail_text = f"🔒 In jail for ~~{format_cooldown_time(original_time, include_emoji=False)}~~ → {format_cooldown_time(remaining_jail, include_emoji=False)} (-20%)"
else:
jail_text = f"🔒 In jail for {format_cooldown_time(remaining_jail)}"
embed.add_field(
name="⚖️ __Jail Status__",
value=jail_text,
inline=False
)
# Show if they've attempted jailbreak this sentence
if member_data.get("attempted_jailbreak", False):
embed.add_field(
name="🔓 __Jailbreak Status__",
value="❌ Already attempted this sentence",
inline=False
)
else:
embed.add_field(
name="⚖️ __Jail Status__",
value="🆓 Not in jail",
inline=False
)
# Add crime cooldowns in a more compact format
cooldowns = []
for crime_type, data in crime_options.items():
if not data.get("enabled", True):
continue
remaining = await self.get_remaining_cooldown(target, crime_type)
crime_name = crime_type.replace('_', ' ').title()
cooldowns.append(
f"{get_crime_emoji(crime_type)} **{crime_name}:** {format_cooldown_time(remaining)}"
)
if cooldowns:
# Split cooldowns into two columns
mid = len(cooldowns) // 2 + len(cooldowns) % 2
embed.add_field(
name="📅 __Crime Cooldowns__",
value="\n".join(cooldowns[:mid]),
inline=True
)
if len(cooldowns) > mid:
embed.add_field(
name="\u200b",
value="\n".join(cooldowns[mid:]),
inline=True
)
# Add empty field to maintain grid layout
embed.add_field(name="\u200b", value="\u200b", inline=True)
# Add notification status
notify_on_release = member_data.get("notify_on_release", False)
notify_unlocked = member_data.get("notify_unlocked", False)
has_reducer = "jail_reducer" in member_data.get("purchased_perks", [])
if notify_unlocked or has_reducer:
status_lines = []
if notify_unlocked:
status_lines.append("🔔 Notifications " + ("enabled" if notify_on_release else "disabled"))
if has_reducer:
status_lines.append("⚖️ Reduced Sentence (-20% jail time)")
embed.add_field(
name="🔰 __Active Perks__",
value="\n".join(status_lines),
inline=True
)
# Add last target if any
if member_data['last_target']:
try:
last_target = await ctx.guild.fetch_member(member_data['last_target'])
if last_target:
embed.add_field(
name="🎯 __Last Target__",
value=last_target.mention,
inline=True
)
except discord.NotFound:
pass
await ctx.send(embed=embed)
except Exception as e:
await ctx.send(_("An error occurred while retrieving the status. Please try again. Error: {}").format(str(e)))
@crime.command(name="stats")
async def crime_stats(self, ctx: commands.Context, user: discord.Member = None):
"""View detailed crime statistics and financial impact
Parameters
----------
user : discord.Member, optional
The user to check stats for. If not provided, shows your own stats."""
try:
# Get member data
target = user or ctx.author
member_data = await self.config.member(target).all()
currency_name = await bank.get_currency_name(ctx.guild)
# Create stats embed
embed = discord.Embed(
title="📊 Criminal Statistics",
description=f"Detailed statistics for {target.mention}",
color=await ctx.embed_color()
)
# Set thumbnail to user's avatar
embed.set_thumbnail(url=target.display_avatar.url)
# Add crime statistics in two columns
stats_left = [
f"**{currency_name} Earned:** {humanize_number(member_data['total_credits_earned'])}",
f"**Crimes:** ✅ {member_data['total_successful_crimes']} | ❌ {member_data['total_failed_crimes']}",
f"**{currency_name} Stolen:** {humanize_number(member_data['total_stolen_from'])}",
f"**Largest Heist:** {humanize_number(member_data['largest_heist'])}",
f"**Highest Streak:** 🔥 {member_data.get('highest_streak', 0)}"
]
# Calculate success rate
total_crimes = member_data['total_successful_crimes'] + member_data['total_failed_crimes']
success_rate = (member_data['total_successful_crimes'] / total_crimes * 100) if total_crimes > 0 else 0
stats_right = [
f"**Total Fines:** {humanize_number(member_data['total_fines_paid'])}",
f"**Total Bail:** {humanize_number(member_data['total_bail_paid'])}",
f"**{currency_name} Lost:** {humanize_number(member_data['total_stolen_by'])}",
f"**Success Rate:** {success_rate:.1f}%" if total_crimes > 0 else "**Success Rate:** N/A"
]
embed.add_field(
name="📊 __Crime Statistics__",
value="\n".join(stats_left),
inline=True
)
embed.add_field(
name="💰 __Financial Impact__",
value="\n".join(stats_right),
inline=True
)
await ctx.send(embed=embed)
except Exception as e:
await ctx.send(_("An error occurred while retrieving the stats. Please try again. Error: {}").format(str(e)))
@crime.command(name="bail")
async def crime_bail(self, ctx: commands.Context):
"""Pay bail to get out of jail early
Bail cost increases with remaining jail time.
Cost is calculated as: remaining_minutes * base_bail_rate
"""
try:
# Check if user is in jail
jail_time = await self.get_jail_time_remaining(ctx.author)
if jail_time <= 0:
await ctx.send(_("You're not in jail!"))
return
# Get settings
settings = await self.config.guild(ctx.guild).global_settings()
if not settings.get("allow_bail", True):
await ctx.send(_("Bail is not allowed in this server!"))
return
# Calculate bail cost based on remaining time
bail_cost = int(settings.get("bail_cost_multiplier", 1.5) * (jail_time / 60)) # Convert seconds to minutes
# Check if user can afford bail
if not await bank.can_spend(ctx.author, bail_cost):
await ctx.send(
_("💵❌You don't have enough {currency} to pay the bail amount of {amount}!").format(
currency=await bank.get_currency_name(ctx.guild),
amount=bail_cost
)
)
return
# Get current balance
current_balance = await bank.get_balance(ctx.author)
currency_name = await bank.get_currency_name(ctx.guild)
# Get member data for perk check
member_data = await self.config.member(ctx.author).all()
# Create embed for bail prompt
embed = discord.Embed(
title="💰 Bail Payment Available",
description=(
"You can pay bail to get out of jail immediately, or wait out your sentence.\n\n"
f"**Time Remaining:** {format_cooldown_time(jail_time, include_emoji=False)}"
+ (" (Reduced by 20%)" if "jail_reducer" in member_data.get("purchased_perks", []) else "") + "\n"
f"**Bail Cost:** {bail_cost:,} {currency_name}\n"
f"**Current Balance:** {current_balance:,} {currency_name}\n\n"
),
color=discord.Color.gold(),
timestamp=discord.utils.utcnow()
)
embed.set_footer(text=f"Requested by {ctx.author.display_name}", icon_url=ctx.author.display_avatar.url)
# Send bail prompt
view = BailView(self, ctx, bail_cost, jail_time)
message = await ctx.send(embed=embed, view=view)
view.message = message
# Reset attempted jailbreak flag when bailing out
async with self.config.member(ctx.author).all() as member_data:
member_data["attempted_jailbreak"] = False
except Exception as e:
await ctx.send(_("An error occurred while processing your bail request. Please try again. Error: {}").format(str(e)))
@crime.command(name="jailbreak")
async def crime_jailbreak(self, ctx: commands.Context):
"""Attempt to break out of jail
Failed attempt increases jail time by 30%!
"""
try:
# Check if user is in jail
jail_time = await self.get_jail_time_remaining(ctx.author)
if jail_time <= 0:
await ctx.send(_("You're not in jail!"))
return
# Get member data
member_data = await self.config.member(ctx.author).all()
# Check if already attempted jailbreak this sentence
if member_data.get("attempted_jailbreak", False):
await ctx.send(_("You've already attempted to break out this sentence!"))
return
# Mark jailbreak as attempted
async with self.config.member(ctx.author).all() as member_data:
member_data["attempted_jailbreak"] = True
# Get random scenario
scenario = get_random_jailbreak_scenario()
success_chance = scenario["base_chance"]
# Send attempt message
attempt_msg = await ctx.send(_(scenario['attempt_text']).format(
user=ctx.author.mention
))
# Add suspense delay
await asyncio.sleep(3)
# Get 1-3 random events
num_events = random.randint(1, 3)
selected_events = random.sample(scenario['events'], num_events)
# Apply events in sequence
for event in selected_events:
event_text = event['text']
# Format event text with currency name
currency_name = await bank.get_currency_name(ctx.guild)
event_text = event_text.format(currency=currency_name)
# Apply chance modifiers and log them
if "chance_bonus" in event:
success_chance = min(1.0, success_chance + event["chance_bonus"]) # Cap at 100%
event_text = f"{event_text}"
elif "chance_penalty" in event:
success_chance = max(0.05, success_chance - event["chance_penalty"]) # Minimum 5% chance
event_text = f"{event_text}"
# Apply currency modifiers
if "currency_bonus" in event:
await bank.deposit_credits(ctx.author, event["currency_bonus"])
event_text += f" (+{event['currency_bonus']} {currency_name})"
elif "currency_penalty" in event:
penalty_amount = event["currency_penalty"]
# Check if user can afford the penalty before withdrawing
if await bank.can_spend(ctx.author, penalty_amount):
await bank.withdraw_credits(ctx.author, penalty_amount)
event_text += f" (-{penalty_amount} {currency_name})"
# else: Optionally add a message here if they couldn't afford it, or just skip silently
await ctx.send(event_text)
await asyncio.sleep(3.5)
roll = random.random()
# Attempt escape
if roll < success_chance:
# Success! Clear jail time
await self.config.member(ctx.author).jail_until.set(0)
await self.config.member(ctx.author).attempted_jailbreak.set(False) # Reset jailbreak attempt when successful
# Cancel any pending release notification
await self._cancel_notification(ctx.author)
# Double check jail time is cleared
remaining = await self.get_jail_time_remaining(ctx.author)
if remaining > 0:
await ctx.send(_("Jail time not properly cleared! Remaining: {}").format(format_cooldown_time(remaining)))
# Force clear it
await self.config.member(ctx.author).jail_until.set(0)
# Create success embed
embed = discord.Embed(
title="🔓 Successful Jailbreak!",
description=_(scenario['success_text']).format(user=ctx.author.mention),
color=discord.Color.green()
)
embed.add_field(
name="🎲 Final Escape Chance",
value=f"{success_chance:.1%}",
inline=True
)
await ctx.send(embed=embed)
else:
# Failed - add 30% more jail time
remaining_time = await self.get_jail_time_remaining(ctx.author)
added_time = int(remaining_time * 0.3) # Add 30% more time
# Get current jail end time and add the additional time
current_jail_until = await self.config.member(ctx.author).jail_until()
new_jail_until = current_jail_until + added_time
await self.config.member(ctx.author).jail_until.set(new_jail_until)
# Create fail embed
embed = discord.Embed(
title="⛓️ Failed Jailbreak!",
description=_(scenario['fail_text']).format(user=ctx.author.mention),
color=discord.Color.red()
)
# Format the time for penalty calculation
minutes = remaining_time // 60
seconds = remaining_time % 60
new_minutes = int((remaining_time * 1.3) // 60)
new_seconds = int((remaining_time * 1.3) % 60)
embed.add_field(
name="⚖️ Penalty",
value=f"Your sentence has been increased by 30%!\n ({minutes}m {seconds}s + 30% = ⏰ {new_minutes}m {new_seconds}s)",
inline=True
)
embed.add_field(
name="🎲 Final Escape Chance",
value=f"{success_chance:.1%}",
inline=True
)
await ctx.send(embed=embed)
except Exception as e:
await ctx.send(_("An error occurred while processing your jailbreak attempt. Please try again. Error: {}").format(str(e)))
@crime.command(name="leaderboard", aliases=["lb"])
@commands.guild_only()
async def crime_leaderboard(self, ctx: commands.Context):
"""View the server's crime leaderboard."""
# Get guild's currency name
currency_name = await bank.get_currency_name(ctx.guild)
stats = {
"earnings": {
"title": f"💰 __Most {currency_name} Earned__",
"field": "total_credits_earned",
"format": "credits"
},
"crimes": {
"title": "🦹 __Crime Success/Fails__",
"fields": ["total_successful_crimes", "total_failed_crimes"],
"format": "counts"
},
"stolen": {
"title": f"💎 __Stolen/Lost {currency_name}__",
"fields": ["total_stolen_from", "total_stolen_by"],
"format": "credits"
},
"largest_heist": {
"title": f"🏆 __Largest Heist__",
"field": "largest_heist",
"format": "credits"
},
"fines": {
"title": f"💸 __Most Fines/Bail Paid__",
"fields": ["total_fines_paid", "total_bail_paid"],
"format": "credits"
},
"streaks": {
"title": "🔥 __Highest Crime Streak__",
"field": "highest_streak",
"format": "number"
}
}
# Get all member data
all_members = await self.config.all_members(ctx.guild)
if not all_members:
return await ctx.send("No crime statistics found for this server!")
embed = discord.Embed(
title="🏆 Crime Leaderboard - Hall of Infamy",
description="The most notorious criminals in the server",
color=await ctx.embed_color()
)
# Medal emojis for top 3
medals = ["🥇", "🥈", "🥉"]
# Process each stat category
field_count = 0 # Track number of non-empty fields
for stat_info in stats.values():
if "fields" in stat_info: # Combined stats
# Sort members by the sum of both fields
sorted_members = sorted(
all_members.items(),
key=lambda x: sum(x[1].get(field, 0) for field in stat_info["fields"]),
reverse=True
)[:3]
if not sorted_members:
continue
field_lines = []
for i, (member_id, data) in enumerate(sorted_members):
member = ctx.guild.get_member(member_id)
if member is None:
continue
if stat_info["format"] == "credits":
if stat_info["title"].startswith("💸"): # Fines category
total_paid = data.get(stat_info['fields'][0], 0) + data.get(stat_info['fields'][1], 0)
field_lines.append(f"{medals[i]} **{member.display_name}** • {humanize_number(total_paid)} {currency_name}")
elif stat_info["title"].startswith("💎"): # Stolen/Lost category
value1 = humanize_number(data.get(stat_info['fields'][0], 0))
value2 = humanize_number(data.get(stat_info['fields'][1], 0))
field_lines.append(f"{medals[i]} **{member.display_name}** • {value1} / {value2}")
else:
value1 = f"{humanize_number(data.get(stat_info['fields'][0], 0))} {currency_name}"
value2 = f"{humanize_number(data.get(stat_info['fields'][1], 0))} {currency_name}"
field_lines.append(f"{medals[i]} **{member.display_name}** • {value1} / {value2}")
else: # counts
wins = data.get(stat_info['fields'][0], 0)
fails = data.get(stat_info['fields'][1], 0)
field_lines.append(f"{medals[i]} **{member.display_name}** • {wins}w / {fails}f")
if field_lines:
embed.add_field(
name=stat_info["title"],
value="\n".join(field_lines),
inline=True
)
field_count += 1
# Add empty field only if we have an odd number of fields and it's not the last field
if field_count % 2 == 1 and field_count < len(stats):
embed.add_field(name="\u200b", value="\u200b", inline=True)
else: # Single stat
sorted_members = sorted(
all_members.items(),
key=lambda x: x[1].get(stat_info["field"], 0),
reverse=True
)[:3]
if not sorted_members:
continue
field_lines = []
for i, (member_id, data) in enumerate(sorted_members):
member = ctx.guild.get_member(member_id)
if member is None:
continue
value = data.get(stat_info["field"], 0)
if stat_info["format"] == "credits":
value_str = f"{humanize_number(value)} {currency_name}"
else:
value_str = str(value)
field_lines.append(f"{medals[i]} **{member.display_name}** • {value_str}")
if field_lines:
embed.add_field(
name=stat_info["title"],
value="\n".join(field_lines),
inline=True
)
field_count += 1
# Add empty field only if we have an odd number of fields and it's not the last field
if field_count % 2 == 1 and field_count < len(stats):
embed.add_field(name="\u200b", value="\u200b", inline=True)
# Add footer with timestamp
embed.set_footer(text=f"Updated")
embed.timestamp = datetime.now()
await ctx.send(embed=embed)
@commands.group(name="crimeset")
@commands.admin_or_permissions(administrator=True)
async def crime_set(self, ctx: commands.Context):
"""Configure crime settings
Commands:
- success_rate: Set success rate for a crime (0.0 to 1.0)
- reward: Set min/max reward for a crime
- cooldown: Set cooldown duration in seconds
- jailtime: Set jail time duration in seconds
- fine: Set fine multiplier for a crime
- global: Configure global settings
"""
pass
@crime_set.command(name="success_rate")
async def set_success_rate(
self, ctx: commands.Context, crime_type: str, rate: float
):
"""Set the success rate for a crime type (0.0 to 1.0)"""
if rate < 0 or rate > 1:
await ctx.send(_("Success rate must be between 0.0 and 1.0"))
return
async with self.config.guild(ctx.guild).crime_options() as crime_options:
if crime_type not in crime_options:
await ctx.send(_("Invalid crime type!"))
return
crime_options[crime_type]["success_rate"] = rate
await ctx.send(_("Success rate for {crime_type} set to {rate}").format(
crime_type=crime_type,
rate=rate
))
@crime_set.command(name="reward")
async def set_reward(
self, ctx: commands.Context, crime_type: str, min_reward: int, max_reward: int
):
"""Set the reward range for a crime type"""
if min_reward < 0 or max_reward < min_reward:
await ctx.send(_("Invalid reward range!"))
return
async with self.config.guild(ctx.guild).crime_options() as crime_options:
if crime_type not in crime_options:
await ctx.send(_("Invalid crime type!"))
return
crime_options[crime_type]["min_reward"] = min_reward
crime_options[crime_type]["max_reward"] = max_reward
await ctx.send(_("Reward range for {crime_type} set to {min_reward}-{max_reward}").format(
crime_type=crime_type,
min_reward=min_reward,
max_reward=max_reward
))
@crime_set.command(name="cooldown")
async def set_cooldown(
self, ctx: commands.Context, crime_type: str, cooldown: int
):
"""Set the cooldown for a crime type (in seconds)"""
if cooldown < 0:
await ctx.send(_("Cooldown must be positive!"))
return
async with self.config.guild(ctx.guild).crime_options() as crime_options:
if crime_type not in crime_options:
await ctx.send(_("Invalid crime type!"))
return
crime_options[crime_type]["cooldown"] = cooldown
await ctx.send(_("Cooldown for {crime_type} set to {time_remaining}").format(
crime_type=crime_type,
time_remaining=format_cooldown_time(cooldown)
))
@crime_set.command(name="jailtime")
async def set_jail_time(
self, ctx: commands.Context, crime_type: str, jail_time: int
):
"""Set the jail time for a crime type (in seconds)"""
if jail_time < 0:
await ctx.send(_("Jail time must be positive!"))
return
async with self.config.guild(ctx.guild).crime_options() as crime_options:
if crime_type not in crime_options:
await ctx.send(_("Invalid crime type!"))
return
crime_options[crime_type]["jail_time"] = jail_time
await ctx.send(_("Jail time for {crime_type} set to {time_remaining}").format(
crime_type=crime_type,
time_remaining=format_cooldown_time(jail_time)
))
@crime_set.command(name="fine")
async def set_fine_multiplier(
self, ctx: commands.Context, crime_type: str, multiplier: float
):
"""Set the fine multiplier for a crime type"""
if multiplier < 0:
await ctx.send(_("Fine multiplier must be positive!"))
return
async with self.config.guild(ctx.guild).crime_options() as crime_options:
if crime_type not in crime_options:
await ctx.send(_("Invalid crime type!"))
return
crime_options[crime_type]["fine_multiplier"] = multiplier
await ctx.send(_("Fine multiplier for {crime_type} set to {multiplier}").format(
crime_type=crime_type,
multiplier=multiplier
))
@crime_set.command(name="reload_defaults")
@commands.admin_or_permissions(administrator=True)
async def reload_crime_defaults(self, ctx: commands.Context):
"""Reload the default crime settings for this guild.
This will update all crime options to match the defaults in data.py.
Warning: This will overwrite any custom settings!
"""
from .data import CRIME_TYPES
# Update crime options with defaults
await self.config.guild(ctx.guild).crime_options.set(CRIME_TYPES.copy())
await ctx.send("✅ Crime settings have been reloaded from defaults!")
@crime_set.group(name="global")
async def crime_set_global(self, ctx: commands.Context):
"""Configure global crime settings
Commands:
- bailcost: Set bail cost multiplier
- togglebail: Enable/disable the bail system
- view: View all current settings
"""
pass
@crime_set_global.command(name="bailcost")
async def set_bail_multiplier(
self, ctx: commands.Context, multiplier: float
):
"""Set the bail cost multiplier"""
if multiplier < 0:
await ctx.send(_("Bail cost multiplier must be positive!"))
return
async with self.config.guild(ctx.guild).global_settings() as settings:
settings["bail_cost_multiplier"] = multiplier
await ctx.send(_("Bail cost multiplier set to {multiplier}").format(
multiplier=multiplier
))
@crime_set_global.command(name="togglebail")
async def toggle_bail(self, ctx: commands.Context, enabled: bool):
"""Enable or disable the bail system"""
async with self.config.guild(ctx.guild).global_settings() as settings:
settings["allow_bail"] = enabled
if enabled:
await ctx.send(_("Bail system enabled!"))
else:
await ctx.send(_("Bail system disabled!"))
@crime_set_global.command(name="view")
async def view_settings(self, ctx: commands.Context):
"""View all crime settings"""
# Get settings
crime_options = await self.config.guild(ctx.guild).crime_options()
global_settings = await self.config.guild(ctx.guild).global_settings()
# Build settings message
settings_lines = [
_("🌐 **Global Settings**:"),
_(" • Bail System: {enabled}").format(enabled=_("Enabled") if global_settings["allow_bail"] else _("Disabled")),
_(" • Bail Cost Multiplier: {multiplier}").format(multiplier=global_settings["bail_cost_multiplier"]),
_(" • Min Steal Balance: {amount}").format(amount=global_settings["min_steal_balance"]),
_(" • Max Steal Amount: {amount}").format(amount=global_settings["max_steal_amount"]),
"",
_("🎯 **Crime Settings**:")
]
for crime_type, data in crime_options.items():
if not data["enabled"]:
continue
settings_lines.extend([
f"\n**{crime_type.title()}**:",
_(" • Success Rate: {rate}").format(rate=data["success_rate"]),
_(" • Reward: {min_reward}-{max_reward}").format(
min_reward=data["min_reward"],
max_reward=data["max_reward"]
),
_(" • Cooldown: {time_remaining}").format(time_remaining=format_cooldown_time(data["cooldown"])),
_(" • Jail Time: {time_remaining}").format(time_remaining=format_cooldown_time(data["jail_time"])),
_(" • Fine Multiplier: {multiplier}").format(multiplier=data["fine_multiplier"])
])
await ctx.send("\n".join(settings_lines))
async def send_to_jail(self, member: discord.Member, jail_time: int, channel: discord.TextChannel = None):
"""Send a member to jail."""
async with self.config.member(member).all() as member_data:
# Check for reduced sentence perk
if "jail_reducer" in member_data.get("purchased_perks", []):
jail_time = int(jail_time * 0.8) # 20% shorter sentence
member_data["jail_until"] = int(time.time()) + jail_time
member_data["attempted_jailbreak"] = False # Reset jailbreak attempt when jailed
# Store the channel ID where they were jailed if provided
if channel:
member_data["jail_channel"] = channel.id
# If notifications are enabled, schedule a notification
if member_data.get("notify_on_release", False):
# Cancel any existing notification task
if member.id in self.notification_tasks:
self.notification_tasks[member.id].cancel()
# Schedule new notification
task = asyncio.create_task(self._schedule_release_notification(member, jail_time))
self.notification_tasks[member.id] = task
async def _cancel_notification(self, member: discord.Member):
"""Cancel any pending release notification for a member."""
if member.id in self.notification_tasks:
self.notification_tasks[member.id].cancel()
del self.notification_tasks[member.id]
async def _schedule_release_notification(self, member: discord.Member, jail_time: int):
"""Schedule a notification for when a member's jail sentence is over."""
await asyncio.sleep(jail_time)
# Double check they're actually out (in case sentence was extended)
remaining = await self.get_jail_time_remaining(member)
if remaining <= 0:
try:
# Only send if they still have notifications enabled
member_data = await self.config.member(member).all()
if member_data.get("notify_on_release", False):
# Try to send to the channel/thread they were jailed in
if "jail_channel" in member_data:
channel_id = member_data["jail_channel"]
# First try to get it as a thread
channel = member.guild.get_thread(channel_id)
# If not a thread, try as a regular channel
if channel is None:
channel = member.guild.get_channel(channel_id)
if channel:
await channel.send(f"🔔 {member.mention} Your jail sentence is over! You're now free to commit crimes again.")
return
# Fallback to DM if channel not found or not stored
await member.send(f"🔔 Your jail sentence is over! You're now free to commit crimes again.")
except (discord.Forbidden, discord.HTTPException):
pass # Ignore if we can't send the message
@crime.command(name="jail")
@commands.admin_or_permissions(administrator=True)
async def manual_jail(self, ctx: commands.Context, user: discord.Member, minutes: int):
"""Manually put a user in jail.
Parameters
----------
user : discord.Member
The user to jail
minutes : int
Number of minutes to jail them for
"""
try:
if minutes <= 0:
await ctx.send("❌ Jail time must be positive!")
return
# Convert minutes to seconds
jail_time = minutes * 60
# Check if user has reduced sentence perk
member_data = await self.config.member(user).all()
has_reducer = "jail_reducer" in member_data.get("purchased_perks", [])
if has_reducer:
jail_time = int(jail_time * 0.8) # 20% reduction
# Send user to jail with the current channel
await self.send_to_jail(user, jail_time, ctx.channel)
embed = discord.Embed(
title="⛓️ Manual Jail",
description=f"{user.mention} has been jailed by {ctx.author.mention}!",
color=discord.Color.red()
)
sentence_text = format_cooldown_time(jail_time)
if has_reducer:
sentence_text += " (Reduced by 20%)"
embed.add_field(
name="⏰ Sentence Duration",
value=sentence_text,
inline=True
)
embed.add_field(
name="📅 Release Time",
value=f"<t:{int(time.time() + jail_time)}:R>",
inline=True
)
await ctx.send(embed=embed)
except Exception as e:
await ctx.send(f"An error occurred while jailing the user: {str(e)}")
@crime.command(name="blackmarket")
async def crime_blackmarket(self, ctx: commands.Context):
"""View the black market shop.
The black market offers special items and perks that can help with your criminal activities.
Items purchased here will appear in your inventory (!city inventory).
"""
from .blackmarket import display_blackmarket
await display_blackmarket(self, ctx)
@crime_set.group(name="scenarios")
@commands.guild_only()
@commands.admin_or_permissions(administrator=True)
async def crime_set_scenarios(self, ctx: commands.Context):
"""Manage custom random scenarios for this server.
Commands:
- add: Add a new custom scenario
- list: List all custom scenarios
- remove: Remove a custom scenario
"""
pass
@crime_set_scenarios.command(name="add")
@commands.guild_only()
@commands.admin_or_permissions(administrator=True)
async def add_scenario(self, ctx: commands.Context):
"""Add a custom random scenario to the crime pool.
This will guide you through creating a custom scenario by asking for:
- Scenario name
- Risk level (low, medium, high)
- Attempt text
- Success text (use {amount} and {currency} placeholders)
- Fail text
Custom scenarios are saved per server and persist through bot restarts.
"""
# Start scenario creation process
await ctx.send("Let's create a new random scenario! I'll ask you for each piece of information.")
try:
# Get scenario name
await ctx.send("What would you like to name this scenario? (e.g. cookie_heist)")
msg = await self.bot.wait_for('message', check=lambda m: m.author == ctx.author and m.channel == ctx.channel, timeout=30)
name = msg.content.lower()
# Get risk level
await ctx.send("What risk level should this be? (low, medium, or high)")
while True:
msg = await self.bot.wait_for('message', check=lambda m: m.author == ctx.author and m.channel == ctx.channel, timeout=30)
risk = msg.content.lower()
if risk not in ["low", "medium", "high"]:
await ctx.send("Please enter either 'low', 'medium', or 'high'.")
else:
break
# Get attempt text
await ctx.send("Enter the attempt text (use {user} for the user's mention):")
msg = await self.bot.wait_for('message', check=lambda m: m.author == ctx.author and m.channel == ctx.channel, timeout=60)
attempt_text = msg.content
# Get success text
await ctx.send("Enter the success text (use {user} for the user's mention, {amount} for the reward amount, and {currency} for the currency name):")
msg = await self.bot.wait_for('message', check=lambda m: m.author == ctx.author and m.channel == ctx.channel, timeout=60)
success_text = msg.content
# Get fail text
await ctx.send("Enter the fail text (use {user} for the user's mention, {fine} for the fine amount, and {currency} for the currency name):")
msg = await self.bot.wait_for('message', check=lambda m: m.author == ctx.author and m.channel == ctx.channel, timeout=60)
fail_text = msg.content
# Set values based on risk level
if risk == "low":
success_rate = 0.7
min_reward = 100
max_reward = 300
jail_time = 180
fine_multiplier = 0.3
elif risk == "medium":
success_rate = 0.5
min_reward = 300
max_reward = 800
jail_time = 300
fine_multiplier = 0.4
else: # high
success_rate = 0.3
min_reward = 800
max_reward = 2000
jail_time = 600
fine_multiplier = 0.5
# Create new scenario
new_scenario = {
"name": name,
"risk": risk,
"min_reward": min_reward,
"max_reward": max_reward,
"success_rate": success_rate,
"jail_time": jail_time,
"fine_multiplier": fine_multiplier,
"attempt_text": attempt_text,
"success_text": success_text,
"fail_text": fail_text
}
# Add to guild's custom scenarios
async with self.config.guild(ctx.guild).custom_scenarios() as scenarios:
scenarios.append(new_scenario)
# Send confirmation
embed = discord.Embed(
title="✅ Custom Scenario Added!",
description=f"Your scenario '{name}' 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 ctx.send(embed=embed)
except asyncio.TimeoutError:
await ctx.send("❌ Scenario creation timed out. Please try again.")
@crime_set_scenarios.command(name="list")
@commands.guild_only()
@commands.admin_or_permissions(administrator=True)
async def list_scenarios(self, ctx: commands.Context):
"""List all custom scenarios in this server."""
custom_scenarios = await self.config.guild(ctx.guild).custom_scenarios()
if not custom_scenarios:
await ctx.send("This server has no custom scenarios.")
return
# Create embed to display scenarios
embed = discord.Embed(
title="📜 Custom Random Scenarios",
description=f"This server has {len(custom_scenarios)} custom scenarios:",
color=await ctx.embed_color()
)
for scenario in custom_scenarios:
# Format success rate as percentage
success_rate = int(scenario["success_rate"] * 100)
# Create field content
details = [
f"**Risk Level:** {scenario['risk'].title()}",
f"**Success Rate:** {success_rate}%",
f"**Reward:** {scenario['min_reward']:,} - {scenario['max_reward']:,}",
f"**Jail Time:** {scenario['jail_time']} seconds",
f"**Fine Multiplier:** {scenario['fine_multiplier']}"
]
embed.add_field(
name=f"🎲 {scenario['name']}",
value="\n".join(details),
inline=False
)
await ctx.send(embed=embed)
@crime_set_scenarios.command(name="remove")
@commands.guild_only()
@commands.admin_or_permissions(administrator=True)
async def remove_scenario(self, ctx: commands.Context, scenario_name: str):
"""Remove a custom scenario by name.
Example:
- [p]crimeset scenarios remove cookie_heist
"""
async with self.config.guild(ctx.guild).custom_scenarios() as scenarios:
# Find scenario with matching name
for i, scenario in enumerate(scenarios):
if scenario["name"].lower() == scenario_name.lower():
removed = scenarios.pop(i)
await ctx.send(f"✅ Removed custom scenario: {removed['name']}")
return
await ctx.send("❌ No custom scenario found with that name.")