Ruby-Cogs/city/utils.py

293 lines
10 KiB
Python

"""Utility functions for the city cog."""
import discord
from redbot.core import bank, commands
from datetime import datetime, timezone
import time
import random
from typing import Optional, Tuple, Union, List, Dict
def format_cooldown_time(seconds: int, include_emoji: bool = True) -> str:
"""Format cooldown time into a human readable string.
Args:
seconds: Number of seconds to format
include_emoji: Whether to include the ⏳ emoji
Returns:
Formatted string like "2h 30m" or "5m 30s"
"""
if seconds <= 0:
return "" if include_emoji else "Ready"
hours = seconds // 3600
minutes = (seconds % 3600) // 60
seconds = seconds % 60
if hours > 0:
time_str = f"{hours}h {minutes}m"
elif minutes > 0:
time_str = f"{minutes}m {seconds}s"
else:
time_str = f"{seconds}s"
return f"{time_str}" if include_emoji else time_str
async def can_target_user(interaction_or_ctx: Union[discord.Interaction, commands.Context], target: discord.Member, action_data: dict, settings: dict) -> Tuple[bool, str]:
"""Base targeting checks for any action.
Args:
interaction_or_ctx: Either a discord.Interaction or commands.Context
target: The member to check if can be targeted
action_data: The action data containing requirements
settings: Global settings containing minimum balance requirements
Returns:
tuple of (can_target, reason)
"""
# Get the author from either interaction or context
if isinstance(interaction_or_ctx, discord.Interaction):
author = interaction_or_ctx.user
else:
author = interaction_or_ctx.author
# Can't target self
if target.id == author.id:
return False, "You can't target yourself!"
# Can't target bots
if target.bot:
return False, "You can't target bots!"
try:
# Check target's balance if minimum is specified
if "min_balance_required" in action_data:
target_balance = await bank.get_balance(target)
min_balance = settings.get("min_steal_balance", 100)
if target_balance < min_balance:
return False, f"Target must have at least {min_balance:,} credits!"
# For actions with steal percentages, check if target has enough based on percentage
if "steal_percentage" in action_data:
steal_percentage = action_data["steal_percentage"]
max_steal = settings.get("max_steal_amount", 1000)
# Calculate potential steal amount
potential_steal = min(int(target_balance * steal_percentage), max_steal)
if potential_steal < action_data.get("min_reward", 0):
return False, f"Target doesn't have enough credits! (Needs at least {int(action_data['min_reward'] / steal_percentage):,} credits)"
except Exception as e:
return False, f"Error checking target's balance: {str(e)}"
return True, ""
async def calculate_stolen_amount(target: discord.Member, crime_data: dict, settings: dict) -> int:
"""Calculate how much to steal from the target based on settings.
Args:
target: The member being stolen from
crime_data: The crime data containing requirements
settings: Global settings containing maximum steal amount
Returns:
Amount to steal, or 0 if error
"""
try:
# Get target's balance
target_balance = await bank.get_balance(target)
# For crimes with steal percentages, calculate based on balance
if "min_steal_percentage" in crime_data and "max_steal_percentage" in crime_data:
# Get random percentage between min and max
steal_percentage = random.uniform(
crime_data["min_steal_percentage"],
crime_data["max_steal_percentage"]
)
# Calculate amount to steal
amount = int(target_balance * steal_percentage)
# Cap at max_steal_amount from settings
max_steal = settings.get("max_steal_amount", 1000)
amount = min(amount, max_steal)
# Cap at crime's max_reward
amount = min(amount, crime_data["max_reward"])
# Ensure at least min_reward if target has enough balance
if amount < crime_data["min_reward"] and target_balance >= (crime_data["min_reward"] / crime_data["min_steal_percentage"]):
amount = crime_data["min_reward"]
return amount
# For other crimes, use normal random range
return random.randint(crime_data["min_reward"], crime_data["max_reward"])
except Exception:
return 0
def get_crime_emoji(crime_type: str) -> str:
"""Get the appropriate emoji for a crime type.
Args:
crime_type: The type of crime (pickpocket, mugging, rob_store, bank_heist, random)
Returns:
The corresponding emoji for the crime type:
• 🧤 for pickpocket
• 🔪 for mugging
• 🏪 for store robbery
• 🏛 for bank heist
• 🎲 for random crimes
"""
emojis = {
"pickpocket": "🧤",
"mugging": "🔪",
"rob_store": "🏪",
"bank_heist": "🏛",
"random": "🎲"
}
return emojis.get(crime_type, "🦹") # Default to generic criminal emoji
def get_risk_emoji(risk_level: str) -> str:
"""Get the emoji for a risk level.
Args:
risk_level: The risk level (low, medium, high)
Returns:
Emoji representing the risk level
"""
emoji_map = {
"low": "🟢",
"medium": "🟡",
"high": "🔴"
}
return emoji_map.get(risk_level, "🤔")
def format_crime_description(crime_type: str, data: dict, cooldown_status: str) -> str:
"""Format a crime's description for display in embeds.
Args:
crime_type: The type of crime
data: Dictionary containing crime data (success_rate, min_reward, max_reward, etc.)
cooldown_status: Formatted cooldown status string
Returns:
A formatted description string containing:
• Success rate percentage
• Reward range (percentage based for pickpocket/mugging)
• Risk level indicator
• Cooldown status
• Whether it requires a target
"""
# Get risk emoji
risk_emoji = "???" if crime_type == "random" else "🟢" if data["risk"] == "low" else "🟡" if data["risk"] == "medium" else "🔴"
# Format description based on crime type
if crime_type == "random":
description = [
"**Success Rate:** ???",
"**Reward:** ???",
f"**Risk Level:** {risk_emoji}",
f"**Cooldown:** {cooldown_status}"
]
elif crime_type == "pickpocket":
description = [
f"**Success Rate:** {int(data['success_rate'] * 100)}%",
"**Reward:** 1-10% of target's balance (max 500)",
f"**Risk Level:** {risk_emoji}",
f"**Cooldown:** {cooldown_status}",
"**Target Required:** Yes"
]
elif crime_type == "mugging":
description = [
f"**Success Rate:** {int(data['success_rate'] * 100)}%",
"**Reward:** 15-25% of target's balance (max 1500)",
f"**Risk Level:** {risk_emoji}",
f"**Cooldown:** {cooldown_status}",
"**Target Required:** Yes"
]
else:
description = [
f"**Success Rate:** {int(data['success_rate'] * 100)}%",
f"**Reward:** {data['min_reward']:,} - {data['max_reward']:,}",
f"**Risk Level:** {risk_emoji}",
f"**Cooldown:** {cooldown_status}"
]
return "\n".join(description)
def calculate_streak_bonus(streak: int) -> float:
"""Calculate reward multiplier based on current streak.
The bonus increases with streak but has diminishing returns:
Streak 1: +5% (1.05x)
Streak 2: +10% (1.10x)
Streak 3: +15% (1.15x)
Streak 4: +20% (1.20x)
Streak 5+: +25% (1.25x) max
"""
if streak <= 0:
return 1.0
# Calculate bonus with diminishing returns
bonus = min(0.25, streak * 0.05) # Cap at 25% bonus
return 1.0 + bonus
async def update_streak(config, member: discord.Member, success: bool) -> tuple[int, float]:
"""Update a member's crime streak and return new streak and multiplier.
Args:
config: The config instance to use for data storage
member (discord.Member): The member to update
success (bool): Whether the crime was successful
Returns:
tuple[int, float]: The new streak count and reward multiplier
"""
async with config.member(member).all() as member_data:
current_time = int(time.time())
last_crime = member_data.get("last_crime_time", 0)
# Reset streak if it's been more than 24 hours since last crime
if current_time - last_crime > 86400: # 24 hours in seconds
member_data["current_streak"] = 0
if success:
# Increment streak on success
member_data["current_streak"] += 1
# Update highest streak if current is higher
if member_data["current_streak"] > member_data.get("highest_streak", 0):
member_data["highest_streak"] = member_data["current_streak"]
else:
# Reset streak on failure
member_data["current_streak"] = 0
# Update last crime time
member_data["last_crime_time"] = current_time
# Calculate new multiplier
new_multiplier = calculate_streak_bonus(member_data["current_streak"])
member_data["streak_multiplier"] = new_multiplier
return member_data["current_streak"], new_multiplier
def format_streak_text(streak: int, multiplier: float) -> str:
"""Format streak information for display in embeds.
Args:
streak (int): Current streak count
multiplier (float): Current reward multiplier
Returns:
str: Formatted streak text
"""
if streak <= 0:
return "No active streak"
bonus_percent = (multiplier - 1.0) * 100
return f"🔥 {streak} streak (+{bonus_percent:.0f}%)"