Revamp README.md with enhanced structure, added features section, and updated credits for cogs used in the Ruby Discord Bot. Included badges for Discord, Python version, and licenses.
Some checks are pending
Run pre-commit / Run pre-commit (push) Waiting to run
Some checks are pending
Run pre-commit / Run pre-commit (push) Waiting to run
This commit is contained in:
parent
48256636da
commit
4495e8cd63
21 changed files with 9244 additions and 30 deletions
92
README.md
92
README.md
|
@ -1,32 +1,64 @@
|
|||
# Ruby Cogs
|
||||
# Ruby Cogs 🤖
|
||||
[](https://discord.gg/red)
|
||||
[](https://www.python.org/)
|
||||
[](LICENSE)
|
||||
[](LICENSE)
|
||||
|
||||
## This repository is being specifically gathered and modified for our instance of Red-DiscordBot
|
||||
- This will contain every cog we use on our instance called 'Ruby', and modified accordingly for our use.
|
||||
## 📝 About
|
||||
This repository contains a curated collection of cogs specifically gathered and modified for our instance of Red-DiscordBot named 'Ruby'. Each cog has been carefully selected and customized to enhance our bot's functionality.
|
||||
|
||||
Credits:
|
||||
[AAA3A Cogs](https://github.com/AAA3A-AAA3A/AAA3A-cogs)
|
||||
[Aikaterna's Cogs](https://github.com/aikaterna/aikaterna-cogs)
|
||||
[Ben Cogs](https://github.com/BenCos17/ben-cogs)
|
||||
[Crab Cogs](https://github.com/hollowstrawberry/crab-cogs)
|
||||
[Dav Cogs](https://github.com/Dav-Git/Dav-Cogs)
|
||||
[Flame Cogs](https://github.com/Flame442/FlameCogs)
|
||||
[Flare Cogs](https://github.com/flaree/Flare-Cogs)
|
||||
[Flare's Pokecord-Red](https://github.com/flaree/pokecord-red)
|
||||
[Fluffy Cogs](https://github.com/zephyrkul/FluffyCogs)
|
||||
[Jojo Cogs](https://github.com/Just-Jojo/JojoCogs)
|
||||
[Kreusada Cogs](https://github.com/kreusada/Kreusada-Cogs)
|
||||
[Kuro Cogs](https://github.com/Kuro-Rui/Kuro-Cogs)
|
||||
[Laggron's Dumb Cogs](https://github.com/laggron42/Laggrons-Dumb-Cogs)
|
||||
[Max Cogs](https://github.com/ltzmax/maxcogs)
|
||||
[MayuYukirin Cogs](https://github.com/skeith/MayuYukirin)
|
||||
[Palmtree5 Cogs](https://github.com/palmtree5/palmtree5-cogs)
|
||||
[PhasecoreX Cogs](https://github.com/PhasecoreX/PCXCogs)
|
||||
[Preda's Cogs](https://github.com/PredaaA/predacogs)
|
||||
[T14D3 Cogs](https://github.com/T14D3/T14D3-Cogs)
|
||||
[Trusty Cogs](https://github.com/TrustyJAID/Trusty-cogs)
|
||||
[Vert Cogs](https://github.com/vertyco/vrt-cogs)
|
||||
[Vex Cogs](https://github.com/Vexed01/Vex-Cogs)
|
||||
[x26 Cogs](https://github.com/Twentysix26/x26-Cogs)
|
||||
[Toxic Cogs](https://github.com/NeuroAssassin/Toxic-Cogs)
|
||||
[Karlo Cogs](https://github.com/karlsbjorn/karlo-cogs)
|
||||
[Mister-42 Cogs](https://github.com/Mister-42/mr42-cogs)
|
||||
## 🎮 Features
|
||||
- Carefully selected cogs from the Red-DiscordBot community
|
||||
- Custom modifications for optimal performance
|
||||
- Regular updates and maintenance
|
||||
- Wide variety of functionality
|
||||
|
||||
## 🙏 Credits
|
||||
We extend our gratitude to the following developers and their amazing contributions:
|
||||
|
||||
### Administrative & Utility
|
||||
- [AAA3A Cogs](https://github.com/AAA3A-AAA3A/AAA3A-cogs) - Comprehensive administrative tools
|
||||
- [Aikaterna's Cogs](https://github.com/aikaterna/aikaterna-cogs) - Versatile utility cogs
|
||||
- [PhasecoreX Cogs](https://github.com/PhasecoreX/PCXCogs) - Enhanced bot management
|
||||
|
||||
### Gaming & Entertainment
|
||||
- [Flare's Pokecord-Red](https://github.com/flaree/pokecord-red) - Pokemon collection and battles
|
||||
- [Flame Cogs](https://github.com/Flame442/FlameCogs) - Gaming and entertainment features
|
||||
- [Calamari Cogs](https://github.com/CalaMariGold/CalaMari-Cogs) - Fun and engaging additions
|
||||
|
||||
### Community & Moderation
|
||||
- [Trusty Cogs](https://github.com/TrustyJAID/Trusty-cogs) - Reliable moderation tools
|
||||
- [Laggron's Dumb Cogs](https://github.com/laggron42/Laggrons-Dumb-Cogs) - Community management
|
||||
- [Vert Cogs](https://github.com/vertyco/vrt-cogs) - Server enhancement tools
|
||||
|
||||
### Additional Resources
|
||||
- [Ben Cogs](https://github.com/BenCos17/ben-cogs)
|
||||
- [Crab Cogs](https://github.com/hollowstrawberry/crab-cogs)
|
||||
- [Dav Cogs](https://github.com/Dav-Git/Dav-Cogs)
|
||||
- [Flare Cogs](https://github.com/flaree/Flare-Cogs)
|
||||
- [Fluffy Cogs](https://github.com/zephyrkul/FluffyCogs)
|
||||
- [Jojo Cogs](https://github.com/Just-Jojo/JojoCogs)
|
||||
- [Kreusada Cogs](https://github.com/kreusada/Kreusada-Cogs)
|
||||
- [Kuro Cogs](https://github.com/Kuro-Rui/Kuro-Cogs)
|
||||
- [Max Cogs](https://github.com/ltzmax/maxcogs)
|
||||
- [MayuYukirin Cogs](https://github.com/skeith/MayuYukirin)
|
||||
- [Palmtree5 Cogs](https://github.com/palmtree5/palmtree5-cogs)
|
||||
- [Preda's Cogs](https://github.com/PredaaA/predacogs)
|
||||
- [T14D3 Cogs](https://github.com/T14D3/T14D3-Cogs)
|
||||
- [Vex Cogs](https://github.com/Vexed01/Vex-Cogs)
|
||||
- [x26 Cogs](https://github.com/Twentysix26/x26-Cogs)
|
||||
- [Toxic Cogs](https://github.com/NeuroAssassin/Toxic-Cogs)
|
||||
- [Karlo Cogs](https://github.com/karlsbjorn/karlo-cogs)
|
||||
- [Mister-42 Cogs](https://github.com/Mister-42/mr42-cogs)
|
||||
|
||||
## 📜 Licenses
|
||||
This repository includes cogs that may be licensed under either:
|
||||
- [MIT License](LICENSE)
|
||||
- [GNU General Public License v3.0 (GPL-3.0)](LICENSE)
|
||||
|
||||
Please check individual cog directories for their specific license information.
|
||||
|
||||
---
|
||||
<div align="center">
|
||||
Made with ❤️ for Ruby Discord Bot
|
||||
</div>
|
120
city/README.md
Normal file
120
city/README.md
Normal file
|
@ -0,0 +1,120 @@
|
|||
# City
|
||||
|
||||
A comprehensive city simulation system with various activities (currently only contains crime module). Features a crime system for earning credits through virtual crimes with different risk levels, cooldowns, rewards, jail system, jail breaks, dynamic events, and custom scenarios.
|
||||
|
||||
## Features
|
||||
|
||||
- 🦹♂️ Multiple crime types with varying risk levels
|
||||
- 🏴☠️ Black Market shop for special items and perks
|
||||
- 🎒 Personal inventory system
|
||||
- ⛓️ Jail system with bail and jailbreak mechanics
|
||||
- 📊 Detailed statistics and leaderboards
|
||||
- 💰 Dynamic reward system
|
||||
- ⚖️ Risk vs. reward gameplay
|
||||
- 🎯 Target other users for crimes
|
||||
- 🎲 Custom random scenarios with server-specific events
|
||||
|
||||
- 🎮 **Button/Dropdown-Based Interface**
|
||||
- Modern, intuitive button controls
|
||||
- Color-coded risk levels
|
||||
- Dynamic button states based on user status
|
||||
- Clean, organized menu system
|
||||
|
||||
- 📊 **Statistics & Leaderboards**
|
||||
- Track successful and failed crimes
|
||||
- Monitor lifetime earnings and largest heists
|
||||
- View server-wide crime leaderboards
|
||||
- Personal criminal status tracking
|
||||
|
||||
## Installation
|
||||
|
||||
1. Make sure you have Red-DiscordBot V3 installed
|
||||
2. Add this repository: `[p]repo add CalaMari-Cogs https://github.com/CalaMariGold/CalaMari-Cogs`
|
||||
3. Install the cog: `[p]cog install CalaMari-Cogs city`
|
||||
|
||||
## Setup
|
||||
|
||||
1. Load the cog: `[p]load city`
|
||||
2. Make sure the bot has required permissions:
|
||||
- Manage Messages (for button interactions)
|
||||
- Send Messages
|
||||
- Embed Links
|
||||
- Add Reactions
|
||||
3. Register your bank if not already done: `[p]bank register`
|
||||
4. Customize settings as desired using the `[p]crimeset` commands
|
||||
|
||||
## Donate
|
||||
|
||||
If you enjoy using this cog, you can support my work:
|
||||
|
||||
[](https://ko-fi.com/calamarigold)
|
||||
|
||||
## Support
|
||||
|
||||
If you encounter any issues or have suggestions, please open an issue on the GitHub repository.
|
||||
|
||||
## Usage
|
||||
|
||||
### Crime Types
|
||||
|
||||
#### 🧤 Pickpocket
|
||||
- Low risk, targeted crime
|
||||
- Steal percentage of target's balance
|
||||
- Quick cooldown
|
||||
- Short jail time if caught
|
||||
|
||||
#### 🔪 Mugging
|
||||
- Medium risk, targeted crime
|
||||
- Steal percentage of target's balance
|
||||
- Medium cooldown
|
||||
- Moderate jail time
|
||||
|
||||
#### 🏪 Store Robbery
|
||||
- Medium risk, non-targeted
|
||||
- Fixed reward range
|
||||
- Medium cooldown
|
||||
- Moderate jail time
|
||||
|
||||
#### 🏛️ Bank Heist
|
||||
- High risk, non-targeted
|
||||
- Highest potential rewards
|
||||
- Long cooldown
|
||||
- Longest jail time
|
||||
|
||||
#### 🎲 Random Crime
|
||||
- Random risk level
|
||||
- Dynamic scenarios (both default and custom)
|
||||
- Varying rewards and penalties
|
||||
- Unique events and outcomes
|
||||
- Server admins can create custom scenarios
|
||||
|
||||
### Commands
|
||||
|
||||
**User Commands:**
|
||||
- `[p]crime` - Open the main crime menu with all available actions
|
||||
- `commit` - Choose and commit a crime
|
||||
- `status` - View your criminal status and stats
|
||||
- `bail` - Pay to get out of jail early
|
||||
- `jailbreak` - Attempt to escape from jail
|
||||
- `leaderboard` - View the server's crime leaderboard
|
||||
|
||||
**Admin Commands:**
|
||||
- `[p]crimeset` - Configure crime settings
|
||||
- `success_rate <crime_type> <rate>` - Set success rate (0.0 to 1.0)
|
||||
- `reward <crime_type> <min> <max>` - Set reward range
|
||||
- `cooldown <crime_type> <seconds>` - Set cooldown duration
|
||||
- `jailtime <crime_type> <seconds>` - Set jail time duration
|
||||
- `fine <crime_type> <multiplier>` - Set fine multiplier
|
||||
- `reload_defaults` - Reset to default settings
|
||||
- `scenarios` - Manage custom random scenarios:
|
||||
- `add` - Create a new custom scenario
|
||||
- `list` - View all custom scenarios
|
||||
- `remove <name>` - Remove a custom scenario
|
||||
- `global` - Configure global settings:
|
||||
- `bailcost <multiplier>` - Set bail cost multiplier
|
||||
- `togglebail <enabled>` - Enable/disable bail system
|
||||
- `view` - View all current settings
|
||||
|
||||
**Owner Commands:**
|
||||
- `[p]wipecitydata <user>` - Wipe a user's city data
|
||||
- `[p]wipecityallusers` - Wipe ALL city data (requires confirmation)
|
30
city/__init__.py
Normal file
30
city/__init__.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
"""City cog for Red-DiscordBot."""
|
||||
|
||||
from redbot.core.bot import Red
|
||||
from redbot.core import commands, bank, Config
|
||||
import discord
|
||||
|
||||
from .base import CityBase
|
||||
from .crime import CrimeCommands
|
||||
from .crime.data import CRIME_TYPES, DEFAULT_GUILD, DEFAULT_MEMBER
|
||||
|
||||
class City(CityBase, CrimeCommands, commands.Cog):
|
||||
"""A virtual city where you can commit crimes, work jobs, and more."""
|
||||
|
||||
def __init__(self, bot: Red) -> None:
|
||||
super().__init__(bot)
|
||||
CrimeCommands.__init__(self, bot)
|
||||
self.bot = bot
|
||||
|
||||
# Store crime types
|
||||
self.crime_types = CRIME_TYPES.copy()
|
||||
|
||||
# Register default guild settings
|
||||
self.config.register_guild(**DEFAULT_GUILD)
|
||||
|
||||
# Register crime-specific member settings
|
||||
self.config.register_member(**DEFAULT_MEMBER)
|
||||
|
||||
async def setup(bot: Red):
|
||||
"""Load City."""
|
||||
await bot.add_cog(City(bot))
|
512
city/base.py
Normal file
512
city/base.py
Normal file
|
@ -0,0 +1,512 @@
|
|||
"""Base cog for city-related features."""
|
||||
|
||||
from redbot.core import commands, bank, Config
|
||||
from redbot.core.bot import Red
|
||||
import discord
|
||||
from datetime import datetime, timezone
|
||||
import time
|
||||
from .crime.data import CRIME_TYPES, DEFAULT_GUILD, DEFAULT_MEMBER
|
||||
from typing import Dict, Any
|
||||
|
||||
CONFIG_SCHEMA = {
|
||||
"GUILD": {
|
||||
"crime_options": {}, # Crime configuration from crime/data.py
|
||||
"global_settings": {}, # Global settings from crime/data.py including:
|
||||
# - Jail and bail settings
|
||||
# - Crime targeting rules
|
||||
# - Notification settings (cost and toggle)
|
||||
"blackmarket_items": {} # Blackmarket items configuration
|
||||
},
|
||||
"MEMBER": {
|
||||
"jail_until": 0, # Unix timestamp when jail sentence ends
|
||||
"last_actions": {}, # Dict mapping action_type -> last_timestamp
|
||||
"total_successful_crimes": 0, # Total number of successful crimes
|
||||
"total_failed_crimes": 0, # Total number of failed crimes
|
||||
"total_fines_paid": 0, # Total amount paid in fines
|
||||
"total_credits_earned": 0, # Total credits earned from all sources
|
||||
"total_stolen_from": 0, # Credits stolen from other users
|
||||
"total_stolen_by": 0, # Credits stolen by other users
|
||||
"total_bail_paid": 0, # Total amount spent on bail
|
||||
"largest_heist": 0, # Largest successful heist amount
|
||||
"last_target": None, # ID of last targeted user (anti-farming)
|
||||
"notify_unlocked": False, # Whether notifications feature is unlocked
|
||||
"notify_on_release": False, # Whether notifications are currently enabled
|
||||
"jail_channel": None, # ID of channel where user was jailed (for notifications)
|
||||
"attempted_jailbreak": False, # Whether user attempted jailbreak this sentence
|
||||
"purchased_perks": [], # List of permanent perks owned from blackmarket
|
||||
"active_items": {} # Dict of temporary items with expiry timestamps or uses
|
||||
}
|
||||
}
|
||||
|
||||
class CityBase:
|
||||
"""Base class for city-related features."""
|
||||
|
||||
def __init__(self, bot: Red) -> None:
|
||||
self.bot = bot
|
||||
self.config = Config.get_conf(
|
||||
self,
|
||||
identifier=95932766180343808,
|
||||
force_registration=True
|
||||
)
|
||||
|
||||
# Register defaults - combine CONFIG_SCHEMA and crime defaults
|
||||
guild_defaults = {**CONFIG_SCHEMA["GUILD"], **DEFAULT_GUILD}
|
||||
member_defaults = {**CONFIG_SCHEMA["MEMBER"], **DEFAULT_MEMBER}
|
||||
|
||||
self.config.register_guild(**guild_defaults)
|
||||
self.config.register_member(**member_defaults)
|
||||
|
||||
# Config schema version
|
||||
self.CONFIG_SCHEMA = 3
|
||||
|
||||
# Track active tasks
|
||||
self.tasks = []
|
||||
|
||||
@commands.group(name="city", invoke_without_command=True)
|
||||
async def city(self, ctx: commands.Context):
|
||||
"""Access the city system."""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help(ctx.command)
|
||||
|
||||
async def red_delete_data_for_user(self, *, requester, user_id: int):
|
||||
"""Delete user data when requested."""
|
||||
# Delete member data
|
||||
await self.config.member_from_ids(None, user_id).clear()
|
||||
|
||||
# Remove user from other members' last_target
|
||||
all_members = await self.config.all_members()
|
||||
for guild_id, guild_data in all_members.items():
|
||||
for member_id, member_data in guild_data.items():
|
||||
if member_data.get("last_target") == user_id:
|
||||
await self.config.member_from_ids(guild_id, member_id).last_target.set(None)
|
||||
|
||||
async def get_jail_time_remaining(self, member: discord.Member) -> int:
|
||||
"""Get remaining jail time in seconds."""
|
||||
jail_until = await self.config.member(member).jail_until()
|
||||
if not jail_until:
|
||||
return 0
|
||||
|
||||
current_time = int(time.time())
|
||||
remaining = max(0, jail_until - current_time)
|
||||
|
||||
# Clear jail if time is up
|
||||
if remaining == 0 and jail_until != 0:
|
||||
await self.config.member(member).jail_until.set(0)
|
||||
|
||||
return remaining
|
||||
|
||||
async def get_remaining_cooldown(self, member: discord.Member, action_type: str) -> int:
|
||||
"""Get remaining cooldown time for an action."""
|
||||
current_time = int(time.time())
|
||||
last_actions = await self.config.member(member).last_actions()
|
||||
|
||||
# Get last attempt time and cooldown
|
||||
last_attempt = last_actions.get(action_type, 0)
|
||||
if not last_attempt:
|
||||
return 0
|
||||
|
||||
# Get crime data for cooldown duration
|
||||
crime_options = await self.config.guild(member.guild).crime_options()
|
||||
if action_type not in crime_options:
|
||||
return 0
|
||||
|
||||
cooldown = crime_options[action_type]["cooldown"]
|
||||
remaining = max(0, cooldown - (current_time - last_attempt))
|
||||
|
||||
return remaining
|
||||
|
||||
async def set_action_cooldown(self, member: discord.Member, action_type: str):
|
||||
"""Set cooldown for an action."""
|
||||
current_time = int(time.time())
|
||||
async with self.config.member(member).last_actions() as last_actions:
|
||||
last_actions[action_type] = current_time
|
||||
|
||||
async def is_jailed(self, member: discord.Member) -> bool:
|
||||
"""Check if a member is currently jailed."""
|
||||
remaining = await self.get_jail_time_remaining(member)
|
||||
return remaining > 0
|
||||
|
||||
async def apply_fine(self, member: discord.Member, crime_type: str, crime_data: dict) -> tuple[bool, int]:
|
||||
"""Apply a fine to a user. Returns (paid_successfully, amount)."""
|
||||
fine_amount = int(crime_data["max_reward"] * crime_data["fine_multiplier"])
|
||||
|
||||
try:
|
||||
# Try to take full fine
|
||||
await bank.withdraw_credits(member, fine_amount)
|
||||
|
||||
# Update stats
|
||||
async with self.config.member(member).all() as member_data:
|
||||
member_data["total_fines_paid"] += fine_amount
|
||||
|
||||
return True, fine_amount
|
||||
except ValueError:
|
||||
# If they can't pay full fine, take what they have
|
||||
try:
|
||||
balance = await bank.get_balance(member)
|
||||
if balance > 0:
|
||||
await bank.withdraw_credits(member, balance)
|
||||
|
||||
# Update stats with partial payment
|
||||
async with self.config.member(member).all() as member_data:
|
||||
member_data["total_fines_paid"] += balance
|
||||
|
||||
return False, fine_amount
|
||||
except Exception as e:
|
||||
return False, fine_amount
|
||||
|
||||
async def handle_target_crime(
|
||||
self,
|
||||
member: discord.Member,
|
||||
target: discord.Member,
|
||||
crime_data: dict,
|
||||
success: bool
|
||||
) -> tuple[int, str]:
|
||||
"""Handle a targeted crime attempt. Returns (amount, message)."""
|
||||
settings = await self.config.guild(member.guild).global_settings()
|
||||
|
||||
if success:
|
||||
# Calculate amount to steal
|
||||
amount = await calculate_stolen_amount(target, crime_data, settings)
|
||||
|
||||
try:
|
||||
# Check target's balance first
|
||||
target_balance = await bank.get_balance(target)
|
||||
if target_balance < amount:
|
||||
return 0, _("Target doesn't have enough {currency}!").format(currency=await bank.get_currency_name(target.guild))
|
||||
|
||||
# Perform the transfer atomically
|
||||
await bank.withdraw_credits(target, amount)
|
||||
await bank.deposit_credits(member, amount)
|
||||
|
||||
# Update stats only after successful transfer
|
||||
async with self.config.member(member).all() as member_data:
|
||||
member_data["total_stolen_from"] += amount
|
||||
member_data["total_credits_earned"] += amount
|
||||
member_data["last_target"] = target.id
|
||||
if amount > member_data["largest_heist"]:
|
||||
member_data["largest_heist"] = amount
|
||||
|
||||
async with self.config.member(target).all() as target_data:
|
||||
target_data["total_stolen_by"] += amount
|
||||
|
||||
return amount, _("🎉 You successfully stole {amount:,} {currency} from {target}!").format(
|
||||
amount=amount,
|
||||
target=target.mention,
|
||||
currency=await bank.get_currency_name(target.guild)
|
||||
)
|
||||
except ValueError as e:
|
||||
return 0, _("Failed to steal credits: Balance changed!")
|
||||
else:
|
||||
# Failed attempt
|
||||
fine_paid, fine_amount = await self.apply_fine(member, crime_data["crime_type"], crime_data)
|
||||
|
||||
# Update stats
|
||||
async with self.config.member(member).all() as member_data:
|
||||
member_data["total_failed_crimes"] += 1
|
||||
|
||||
if fine_paid:
|
||||
return 0, _("💀 You were caught trying to steal from {target}! You paid a fine of {fine:,} {currency} and were sent to jail for {minutes}m!").format(
|
||||
target=target.display_name,
|
||||
fine=fine_amount,
|
||||
minutes=crime_data["jail_time"] // 60,
|
||||
currency=await bank.get_currency_name(target.guild)
|
||||
)
|
||||
else:
|
||||
return 0, _("💀 You were caught and couldn't pay the {fine:,} credit fine! You were sent to jail for {minutes}m!").format(
|
||||
fine=fine_amount,
|
||||
minutes=crime_data["jail_time"] // 60
|
||||
)
|
||||
|
||||
async def cog_unload(self):
|
||||
"""Clean up when cog is unloaded."""
|
||||
for task in self.tasks:
|
||||
task.cancel()
|
||||
class ConfirmWipeView(discord.ui.View):
|
||||
def __init__(self, ctx: commands.Context, user: discord.Member):
|
||||
super().__init__(timeout=30.0) # 30 second timeout
|
||||
self.ctx = ctx
|
||||
self.user = user
|
||||
self.value = None
|
||||
|
||||
async def interaction_check(self, interaction: discord.Interaction) -> bool:
|
||||
# Only allow the original command author to use the buttons
|
||||
return interaction.user.id == self.ctx.author.id
|
||||
|
||||
@discord.ui.button(label='Confirm Wipe', style=discord.ButtonStyle.danger)
|
||||
async def confirm(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
self.value = True
|
||||
self.stop()
|
||||
# Disable all buttons after clicking
|
||||
for item in self.children:
|
||||
item.disabled = True
|
||||
await interaction.response.edit_message(view=self)
|
||||
|
||||
@discord.ui.button(label='Cancel', style=discord.ButtonStyle.grey)
|
||||
async def cancel(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
self.value = False
|
||||
self.stop()
|
||||
# Disable all buttons after clicking
|
||||
for item in self.children:
|
||||
item.disabled = True
|
||||
await interaction.response.edit_message(view=self)
|
||||
|
||||
async def on_timeout(self) -> None:
|
||||
# Disable all buttons if the view times out
|
||||
for item in self.children:
|
||||
item.disabled = True
|
||||
# Try to update the message with disabled buttons
|
||||
try:
|
||||
await self.message.edit(view=self)
|
||||
except discord.NotFound:
|
||||
pass
|
||||
|
||||
class ConfirmGlobalWipeView(discord.ui.View):
|
||||
def __init__(self, ctx: commands.Context):
|
||||
super().__init__(timeout=30.0) # 30 second timeout
|
||||
self.ctx = ctx
|
||||
self.value = None
|
||||
self.confirmation_phrase = None
|
||||
self.waiting_for_confirmation = False
|
||||
|
||||
async def interaction_check(self, interaction: discord.Interaction) -> bool:
|
||||
# Only allow the original command author to use the buttons
|
||||
return interaction.user.id == self.ctx.author.id
|
||||
|
||||
@discord.ui.button(label='I Understand - Proceed to Confirmation', style=discord.ButtonStyle.danger)
|
||||
async def confirm_understanding(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
if self.waiting_for_confirmation:
|
||||
return
|
||||
|
||||
self.waiting_for_confirmation = True
|
||||
|
||||
# Generate a random confirmation phrase
|
||||
import random
|
||||
import string
|
||||
import asyncio
|
||||
self.confirmation_phrase = ''.join(random.choices(string.ascii_uppercase, k=6))
|
||||
|
||||
# Disable the initial button
|
||||
button.disabled = True
|
||||
# Remove the cancel button if it exists
|
||||
cancel_button = discord.utils.get(self.children, label='Cancel')
|
||||
if cancel_button:
|
||||
self.remove_item(cancel_button)
|
||||
|
||||
# Add the final confirmation button
|
||||
confirm_button = discord.ui.Button(
|
||||
label=f'CONFIRM WIPE - Type "{self.confirmation_phrase}"',
|
||||
style=discord.ButtonStyle.danger,
|
||||
disabled=True,
|
||||
custom_id='final_confirm'
|
||||
)
|
||||
self.add_item(confirm_button)
|
||||
|
||||
await interaction.response.edit_message(
|
||||
content=f"⚠️ **FINAL WARNING**\n\n"
|
||||
f"To proceed with wiping ALL city data for ALL users, you must type:\n"
|
||||
f"```\n{self.confirmation_phrase}\n```\n"
|
||||
f"This will permanently delete all user stats, crime records, and other city data.\n"
|
||||
f"You have 30 seconds to confirm.",
|
||||
view=self
|
||||
)
|
||||
|
||||
# Start listening for the confirmation message
|
||||
def check(m) -> bool:
|
||||
return m.author.id == self.ctx.author.id and m.channel.id == self.ctx.channel.id and m.content.lower() == "confirm"
|
||||
|
||||
try:
|
||||
await self.ctx.bot.wait_for('message', timeout=30.0, check=check)
|
||||
self.value = True
|
||||
except asyncio.TimeoutError:
|
||||
self.value = None
|
||||
finally:
|
||||
self.stop()
|
||||
# Try to update the message one last time
|
||||
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='Cancel', style=discord.ButtonStyle.grey)
|
||||
async def cancel(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
if self.waiting_for_confirmation:
|
||||
return
|
||||
|
||||
self.value = False
|
||||
self.stop()
|
||||
# Disable all buttons after clicking
|
||||
for item in self.children:
|
||||
item.disabled = True
|
||||
await interaction.response.edit_message(view=self)
|
||||
|
||||
async def on_timeout(self) -> None:
|
||||
# Disable all buttons if the view times out
|
||||
for item in self.children:
|
||||
item.disabled = True
|
||||
# Try to update the message with disabled buttons
|
||||
try:
|
||||
await self.message.edit(view=self)
|
||||
except (discord.NotFound, discord.HTTPException):
|
||||
pass
|
||||
|
||||
@commands.command()
|
||||
@commands.is_owner()
|
||||
async def wipecitydata(self, ctx: commands.Context, user: discord.Member):
|
||||
"""Wipe all city-related data for a specific user.
|
||||
|
||||
This will remove all their stats, including:
|
||||
- Crime records and cooldowns
|
||||
- Jail status and history
|
||||
- All statistics (successful crimes, failed crimes, etc.)
|
||||
- All perks and items
|
||||
- References in other users' data
|
||||
|
||||
This action cannot be undone.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
user: discord.Member
|
||||
The user whose data should be wiped
|
||||
"""
|
||||
# Create confirmation view
|
||||
view = self.ConfirmWipeView(ctx, user)
|
||||
view.message = await ctx.send(
|
||||
f"⚠️ Are you sure you want to wipe all city data for {user.display_name}?\n"
|
||||
"This action cannot be undone and will remove all their stats, including:\n"
|
||||
"• Crime records and cooldowns\n"
|
||||
"• Jail status and history\n"
|
||||
"• All statistics (successful crimes, failed crimes, etc.)\n"
|
||||
"• All perks and items\n"
|
||||
"• References in other users' data",
|
||||
view=view
|
||||
)
|
||||
|
||||
# Wait for the user's response
|
||||
await view.wait()
|
||||
|
||||
if view.value is None:
|
||||
await ctx.send("❌ Wipe cancelled - timed out.")
|
||||
return
|
||||
elif view.value is False:
|
||||
await ctx.send("❌ Wipe cancelled.")
|
||||
return
|
||||
|
||||
try:
|
||||
# Step 1: Clear all member data across all guilds
|
||||
all_guilds = self.bot.guilds
|
||||
for guild in all_guilds:
|
||||
await self.config.member_from_ids(guild.id, user.id).clear()
|
||||
|
||||
# Step 2: Remove user from other members' data
|
||||
all_members = await self.config.all_members()
|
||||
for guild_id, guild_data in all_members.items():
|
||||
for member_id, member_data in guild_data.items():
|
||||
if member_id == user.id:
|
||||
continue # Skip the user's own data as it's already cleared
|
||||
|
||||
modified = False
|
||||
# Check last_target
|
||||
if member_data.get("last_target") == user.id:
|
||||
await self.config.member_from_ids(guild_id, member_id).last_target.set(None)
|
||||
modified = True
|
||||
|
||||
await ctx.send(f"✅ Successfully wiped all city data for {user.display_name} across all guilds.")
|
||||
|
||||
except Exception as e:
|
||||
await ctx.send(f"❌ An error occurred while wiping data: {str(e)}")
|
||||
|
||||
@commands.command()
|
||||
@commands.is_owner()
|
||||
async def wipecityallusers(self, ctx: commands.Context):
|
||||
"""Wipe ALL city-related data for ALL users.
|
||||
|
||||
This is an extremely destructive action that will:
|
||||
- Delete ALL user stats
|
||||
- Remove ALL crime records and cooldowns
|
||||
- Clear ALL jail status and history
|
||||
- Wipe ALL perks and items
|
||||
- Remove ALL cross-user references
|
||||
- Remove ALL data across ALL guilds
|
||||
|
||||
This action absolutely cannot be undone.
|
||||
"""
|
||||
# Create confirmation view
|
||||
view = self.ConfirmGlobalWipeView(ctx)
|
||||
view.message = await ctx.send(
|
||||
"🚨 **GLOBAL CITY DATA WIPE** 🚨\n\n"
|
||||
"You are about to wipe ALL city data for ALL users across ALL guilds.\n\n"
|
||||
"This will permanently delete:\n"
|
||||
"• All user statistics\n"
|
||||
"• All crime records and cooldowns\n"
|
||||
"• All jail records and history\n"
|
||||
"• All perks and items\n"
|
||||
"• All cross-user references\n"
|
||||
"• All other city-related data\n\n"
|
||||
"This action CANNOT be undone and will affect ALL users.\n"
|
||||
"Are you sure you want to proceed?",
|
||||
view=view
|
||||
)
|
||||
|
||||
# Wait for the user's response
|
||||
await view.wait()
|
||||
|
||||
if view.value is None:
|
||||
await ctx.send("❌ Global wipe cancelled - timed out.")
|
||||
return
|
||||
elif view.value is False:
|
||||
await ctx.send("❌ Global wipe cancelled.")
|
||||
return
|
||||
|
||||
try:
|
||||
# Step 1: Clear all member data across all guilds
|
||||
all_members = await self.config.all_members()
|
||||
count = 0
|
||||
for guild_id, guild_data in all_members.items():
|
||||
for member_id in guild_data.keys():
|
||||
await self.config.member_from_ids(guild_id, member_id).clear()
|
||||
count += 1
|
||||
|
||||
# Step 2: Clear all guild settings to defaults
|
||||
guild_count = 0
|
||||
all_guilds = self.bot.guilds
|
||||
for guild in all_guilds:
|
||||
await self.config.guild(guild).clear()
|
||||
guild_count += 1
|
||||
|
||||
await ctx.send(f"✅ Successfully wiped ALL city data:\n"
|
||||
f"• Cleared data for {count:,} users\n"
|
||||
f"• Reset settings in {guild_count:,} guilds")
|
||||
|
||||
except Exception as e:
|
||||
await ctx.send(f"❌ An error occurred while wiping data: {str(e)}")
|
||||
|
||||
@city.command(name="inventory")
|
||||
@commands.guild_only()
|
||||
async def city_inventory(self, ctx: commands.Context) -> None:
|
||||
"""View your inventory of items and perks from all city systems.
|
||||
|
||||
This command displays a combined view of all items and perks you own
|
||||
from different city systems like crime, business, etc.
|
||||
"""
|
||||
# Gather items from different systems
|
||||
all_items: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
# Add crime items if crime system is loaded
|
||||
try:
|
||||
from .crime.blackmarket import BLACKMARKET_ITEMS
|
||||
all_items.update(BLACKMARKET_ITEMS)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Add business items if business system is loaded
|
||||
try:
|
||||
from .business.shop import BUSINESS_ITEMS
|
||||
all_items.update(BUSINESS_ITEMS)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Display combined inventory
|
||||
from .inventory import display_inventory
|
||||
await display_inventory(self, ctx, all_items)
|
165
city/business_system_design.md
Normal file
165
city/business_system_design.md
Normal file
|
@ -0,0 +1,165 @@
|
|||
# Business System - City Cog
|
||||
|
||||
## Overview
|
||||
The business system is a dynamic economic feature that allows players to establish and manage their own businesses within the city. Players can earn passive income, upgrade their establishments, and engage in strategic risk management. The system incorporates elements of passive income generation, player interaction through robberies (via the crime module), and progressive advancement through multiple business levels and industries.
|
||||
|
||||
Key aspects:
|
||||
- Passive income through a vault-based system
|
||||
- Three distinct industry types with unique mechanics
|
||||
- Upgradeable business levels with increasing benefits
|
||||
- Strategic security vs. profit decisions
|
||||
- Interactive robbery system for player engagement
|
||||
- Shop system with progressive unlocks
|
||||
|
||||
## 🏢 Core Features
|
||||
|
||||
### 💰 Passive Income Generation
|
||||
- Deposit credits into your business vault
|
||||
- Earn daily profits based on vault balance
|
||||
- Business vault can be robbed by players, stealing a percentage of the vault balance
|
||||
- Strategic risk vs. reward management
|
||||
|
||||
### 🏦 Vault System
|
||||
- Put credits in your vault to earn profit
|
||||
- Every hour you get 1/24th of your daily rate
|
||||
- Profits automatically add to your vault
|
||||
- More credits in vault = more profit per hour
|
||||
- Anyone can try to rob your vault
|
||||
- Vault has a maximum capacity based on level
|
||||
- Choose between collecting profits or letting them compound
|
||||
|
||||
Example:
|
||||
- You have a Level 1 business (2% daily rate)
|
||||
- You put 10,000 credits in your vault
|
||||
- Each hour you earn: 10,000 × (2% ÷ 24) = 8.33 credits
|
||||
- After one day: ~200 credits profit
|
||||
- Profits add to your vault automatically every hour
|
||||
|
||||
### ⬆️ Business Progression and Shop
|
||||
- Multiple upgrade levels with increased vault capacity
|
||||
- Better income generation rates at higher levels
|
||||
- Access to better shop items, including security upgrades
|
||||
- Level-gated special items
|
||||
- Mix of defensive and profit-focused items
|
||||
|
||||
## Business Industries
|
||||
|
||||
Choose your industry when starting (expensive to change later):
|
||||
|
||||
### 💹 Trading
|
||||
- Daily profit rate fluctuates between -10% to +10% of base rate, +5% both ways for each level
|
||||
- Rate changes at midnight server time
|
||||
- 24h lockup period after deposits (no withdrawals)
|
||||
- Each day with increased profit adds +1% to base rate (max +10% bonus)
|
||||
- Bonus persists until a day with negative rate
|
||||
- Best for players who enjoy active management and timing
|
||||
- Example: At level 1 (2% base), rate varies from 1.7% to 2.3% daily
|
||||
|
||||
### 🏭 Manufacturing
|
||||
- Base profit rate: -1% from the base rate (1% profit rate at lv 1)
|
||||
- Vault capacity +25% (Level 1: 37.5k instead of 30k)
|
||||
- Robbery steal % reduced by 15% (steal 8.5-21.25% instead of 10-25%)
|
||||
- Consecutive days without withdrawal adds +0.5% profit (max +10%)
|
||||
- Best for players who prefer steady, long-term growth
|
||||
|
||||
### 🎯 Retail
|
||||
- Base profit rate +35%
|
||||
- Robbery success rate increases by 15%
|
||||
- Each successful robbery reduces profit rate by 5% for 24hr
|
||||
- Robbery penalties stack up to -15% maximum
|
||||
- Best for players who prefer high risk, high reward
|
||||
|
||||
## Business Levels
|
||||
|
||||
### Level 1
|
||||
- Max vault: 30,000 credits
|
||||
- Income rate: 2% daily
|
||||
- Access to basic shop items
|
||||
|
||||
### Level 2
|
||||
- Max vault: 75,000 credits
|
||||
- Income rate: 2.5% daily
|
||||
- Unlocks intermediate shop items
|
||||
|
||||
### Level 3
|
||||
- Max vault: 150,000 credits
|
||||
- Income rate: 3% daily
|
||||
- Unlocks advanced shop items
|
||||
|
||||
### Level 4
|
||||
- Max vault: 300,000 credits
|
||||
- Income rate: 3.5% daily
|
||||
- Unlocks premium shop items
|
||||
|
||||
## Shop Items
|
||||
|
||||
### Level 1 Items
|
||||
- 📷 Security Camera (-10% robbery success) - 10,000 credits
|
||||
- 🚨 Basic Alarm (-10% robbery success) - 15,000 credits
|
||||
|
||||
### Level 2 Unlocks
|
||||
- 💂 Security Guard (-15% robbery success) - 25,000 credits
|
||||
- 🏦 Vault Insurance (recover 25% of stolen credits) - 35,000 credits
|
||||
|
||||
### Level 3 Unlocks
|
||||
- 🛡️ Advanced Security System (-20% robbery success) - 50,000 credits
|
||||
- 📊 Risk Management (reduce max steal % by 5) - 75,000 credits
|
||||
|
||||
### Level 4 Unlocks
|
||||
- 🔐 Premium Vault (25% of the vault is not considered for robbery percentage) - 100,000 credits
|
||||
- 📈 Market Analyzer (+10% profit rate) - 125,000 credits
|
||||
|
||||
## Commands
|
||||
|
||||
### Business Owner Commands
|
||||
- `[p]business` - Open the main business menu
|
||||
- `start <business name> <industry>` - Start a new business
|
||||
- `deposit <amount>` - Add credits to vault
|
||||
- `withdraw <amount>` - Remove credits from vault
|
||||
- `upgrade` - Level up your business
|
||||
- `shop` - View and purchase items
|
||||
- `status` - View business stats and info, including profit generated in the last 24 hours
|
||||
- `changeindustry <new_industry>` - Change business industry (costs 50% of your current max vault capacity)
|
||||
|
||||
### Criminal Commands
|
||||
- `[p]crime commit business <target>` - Rob a business
|
||||
- Shows target's business level
|
||||
- Displays visible security measures
|
||||
- Indicates approximate vault balance
|
||||
- Shows success chance and potential steal amount
|
||||
|
||||
## System Mechanics
|
||||
|
||||
### Starting Out
|
||||
- Players start a business with initial investment
|
||||
- Choose an industry type that fits their playstyle
|
||||
- Begin with basic shop items available
|
||||
|
||||
### Income Generation
|
||||
- Larger vault balance = more daily profit
|
||||
- Profits automatically add to vault balance
|
||||
- All stored money (including profits) can be stolen
|
||||
- Must balance growth vs. risk
|
||||
|
||||
### Business Robbery
|
||||
- Criminals can target a business vault once every 24 hours
|
||||
- Base steal range: 5-15% of vault balance
|
||||
- Vaults need a minimum of 2500 credits to be robbed
|
||||
- Success chance calculation:
|
||||
- Start with 50% base chance
|
||||
- Add/subtract industry modifiers
|
||||
- Subtract target's security items
|
||||
- Add criminal's success rate items
|
||||
- Steal percentage calculation:
|
||||
- Start with random 5-15% base steal
|
||||
- Subtract security item effects (Risk Management: -5%)
|
||||
- Subtract industry effects (Manufacturing: -15%)
|
||||
- Apply to vault balance
|
||||
- Example:
|
||||
- Manufacturing business with Risk Management
|
||||
|
||||
### Progression
|
||||
- Upgrade business level for better benefits
|
||||
- Purchase shop items for security and profit
|
||||
- Manage vault balance strategically
|
||||
- Change industry for 50% of max vault capacity (Level 1: 15k, Level 2: 37.5k, Level 3: 75k, Level 4: 150k)
|
7
city/crime/__init__.py
Normal file
7
city/crime/__init__.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
"""Crime module for the City cog."""
|
||||
|
||||
from .commands import CrimeCommands
|
||||
from .data import CRIME_TYPES, DEFAULT_GUILD, DEFAULT_MEMBER
|
||||
from .views import CrimeView, BailView, TargetSelectionView
|
||||
|
||||
__all__ = ["CrimeCommands", "CRIME_TYPES", "DEFAULT_GUILD", "DEFAULT_MEMBER", "CrimeView", "BailView", "TargetSelectionView"]
|
224
city/crime/blackmarket.py
Normal file
224
city/crime/blackmarket.py
Normal file
|
@ -0,0 +1,224 @@
|
|||
"""Blackmarket system for the City cog.
|
||||
|
||||
This module provides a shop interface for purchasing special items and perks
|
||||
that can be used to gain advantages in the crime system.
|
||||
|
||||
The blackmarket offers both permanent perks and consumable items that can
|
||||
affect various aspects of the crime system, such as jail time and notifications.
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, Optional
|
||||
import discord
|
||||
from redbot.core import bank, commands
|
||||
|
||||
# Registry of available blackmarket items
|
||||
BLACKMARKET_ITEMS: Dict[str, Dict[str, Any]] = {
|
||||
"notify_ping": {
|
||||
"name": "Jail Release Notification",
|
||||
"description": "Get notified when your jail sentence is over",
|
||||
"cost": 10000,
|
||||
"type": "perk",
|
||||
"emoji": "🔔",
|
||||
"can_sell": True,
|
||||
"toggleable": True
|
||||
},
|
||||
"jail_reducer": {
|
||||
"name": "Reduced Sentence",
|
||||
"description": "Permanently reduce jail time by 20%",
|
||||
"cost": 20000,
|
||||
"type": "perk",
|
||||
"emoji": "⚖️",
|
||||
"can_sell": True,
|
||||
"toggleable": False
|
||||
},
|
||||
"jail_pass": {
|
||||
"name": "Get Out of Jail Free",
|
||||
"description": "Instantly escape from jail",
|
||||
"cost": 1000,
|
||||
"type": "consumable",
|
||||
"emoji": "🔑",
|
||||
"can_sell": True,
|
||||
"uses": 1
|
||||
}
|
||||
}
|
||||
|
||||
class BlackmarketView(discord.ui.View):
|
||||
"""A view for displaying and purchasing items from the blackmarket.
|
||||
|
||||
This view provides a shop interface where users can view and purchase
|
||||
various items and perks that affect the crime system.
|
||||
|
||||
Attributes:
|
||||
ctx (commands.Context): The command context
|
||||
cog (commands.Cog): The cog instance that created this view
|
||||
message (Optional[discord.Message]): The message containing this view
|
||||
"""
|
||||
|
||||
def __init__(self, ctx: commands.Context, cog: commands.Cog) -> None:
|
||||
super().__init__(timeout=180)
|
||||
self.ctx = ctx
|
||||
self.cog = cog
|
||||
self.message: Optional[discord.Message] = None
|
||||
self._update_options()
|
||||
|
||||
def _update_options(self) -> None:
|
||||
"""Update the select menu options based on available items."""
|
||||
# Clear existing options
|
||||
for child in self.children[:]:
|
||||
self.remove_item(child)
|
||||
|
||||
# Create select menu
|
||||
select = discord.ui.Select(
|
||||
placeholder="Select an item to purchase",
|
||||
options=[
|
||||
discord.SelectOption(
|
||||
label=item["name"],
|
||||
value=item_id,
|
||||
description=f"{item['description']} - {item['cost']:,} credits",
|
||||
emoji=item["emoji"]
|
||||
)
|
||||
for item_id, item in BLACKMARKET_ITEMS.items()
|
||||
]
|
||||
)
|
||||
select.callback = self._handle_purchase
|
||||
self.add_item(select)
|
||||
|
||||
async def _handle_purchase(self, interaction: discord.Interaction) -> None:
|
||||
"""Handle item purchase.
|
||||
|
||||
Args:
|
||||
interaction (discord.Interaction): The interaction that triggered this callback
|
||||
"""
|
||||
if interaction.user.id != self.ctx.author.id:
|
||||
await interaction.response.send_message("This menu is not for you!", ephemeral=True)
|
||||
return
|
||||
|
||||
try:
|
||||
item_id = interaction.data["values"][0]
|
||||
item = BLACKMARKET_ITEMS[item_id]
|
||||
except (KeyError, AttributeError):
|
||||
await interaction.response.send_message(
|
||||
"❌ This item is no longer available!",
|
||||
ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
# Check if user can afford
|
||||
balance = await bank.get_balance(self.ctx.author)
|
||||
if balance < item["cost"]:
|
||||
currency_name = await bank.get_currency_name(self.ctx.guild)
|
||||
await interaction.response.send_message(
|
||||
f"❌ You need {item['cost']:,} {currency_name} to buy this!",
|
||||
ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
# Handle purchase
|
||||
async with self.cog.config.member(self.ctx.author).all() as member_data:
|
||||
if item["type"] == "perk":
|
||||
if item_id in member_data.get("purchased_perks", []):
|
||||
await interaction.response.send_message(
|
||||
"❌ You already own this perk!",
|
||||
ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
if "purchased_perks" not in member_data:
|
||||
member_data["purchased_perks"] = []
|
||||
member_data["purchased_perks"].append(item_id)
|
||||
|
||||
# Special handling for notify_ping
|
||||
if item_id == "notify_ping":
|
||||
member_data["notify_unlocked"] = True
|
||||
member_data["notify_on_release"] = True
|
||||
else: # consumable
|
||||
if "active_items" not in member_data:
|
||||
member_data["active_items"] = {}
|
||||
|
||||
if item_id in member_data["active_items"]:
|
||||
current_uses = member_data["active_items"][item_id].get("uses", 0)
|
||||
if current_uses > 0:
|
||||
await interaction.response.send_message(
|
||||
"❌ You already have this item with uses remaining!",
|
||||
ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
member_data["active_items"][item_id] = {"uses": item["uses"]}
|
||||
|
||||
# Complete purchase
|
||||
await bank.withdraw_credits(self.ctx.author, item["cost"])
|
||||
currency_name = await bank.get_currency_name(self.ctx.guild)
|
||||
|
||||
await interaction.response.send_message(
|
||||
f"✅ Purchased {item['emoji']} **{item['name']}** for {item['cost']:,} {currency_name}",
|
||||
ephemeral=True
|
||||
)
|
||||
|
||||
async def interaction_check(self, interaction: discord.Interaction) -> bool:
|
||||
"""Check if the interaction is valid.
|
||||
|
||||
Args:
|
||||
interaction (discord.Interaction): The interaction to check
|
||||
|
||||
Returns:
|
||||
bool: True if the interaction is valid, False otherwise
|
||||
"""
|
||||
return interaction.user.id == self.ctx.author.id
|
||||
|
||||
async def on_timeout(self) -> None:
|
||||
"""Handle view timeout by disabling all components."""
|
||||
try:
|
||||
for child in self.children:
|
||||
child.disabled = True
|
||||
await self.message.edit(view=self)
|
||||
except (discord.NotFound, discord.HTTPException):
|
||||
pass
|
||||
|
||||
async def display_blackmarket(cog: commands.Cog, ctx: commands.Context) -> None:
|
||||
"""Display the blackmarket shop interface.
|
||||
|
||||
Args:
|
||||
cog (commands.Cog): The cog instance
|
||||
ctx (commands.Context): The command context
|
||||
"""
|
||||
currency_name = await bank.get_currency_name(ctx.guild)
|
||||
|
||||
# Create embed
|
||||
embed = discord.Embed(
|
||||
title="🏴☠️ Black Market",
|
||||
description="Welcome to the black market! Here you can purchase special items and perks.",
|
||||
color=discord.Color.dark_red()
|
||||
)
|
||||
|
||||
# Add perks section
|
||||
perks = []
|
||||
for item_id, item in BLACKMARKET_ITEMS.items():
|
||||
if item["type"] == "perk":
|
||||
perks.append(f"{item['emoji']} **{item['name']}** - {item['cost']:,} {currency_name}\n"
|
||||
f"↳ {item['description']}")
|
||||
|
||||
if perks:
|
||||
embed.add_field(
|
||||
name="__🔒 Permanent Perks__",
|
||||
value="\n".join(perks),
|
||||
inline=False
|
||||
)
|
||||
|
||||
# Add consumable items section
|
||||
items = []
|
||||
for item_id, item in BLACKMARKET_ITEMS.items():
|
||||
if item["type"] == "consumable":
|
||||
items.append(f"{item['emoji']} **{item['name']}** - {item['cost']:,} {currency_name}\n"
|
||||
f"↳ {item['description']}")
|
||||
|
||||
if items:
|
||||
embed.add_field(
|
||||
name="__📦 Consumable Items__",
|
||||
value="\n".join(items),
|
||||
inline=False
|
||||
)
|
||||
|
||||
# Create and send view
|
||||
view = BlackmarketView(ctx, cog)
|
||||
view.message = await ctx.send(embed=embed, view=view)
|
1238
city/crime/commands.py
Normal file
1238
city/crime/commands.py
Normal file
File diff suppressed because it is too large
Load diff
105
city/crime/data.py
Normal file
105
city/crime/data.py
Normal file
|
@ -0,0 +1,105 @@
|
|||
"""Data and settings for the crime system."""
|
||||
|
||||
# Define available crimes and their properties
|
||||
CRIME_TYPES = {
|
||||
"pickpocket": {
|
||||
"requires_target": True,
|
||||
"min_reward": 150,
|
||||
"max_reward": 500,
|
||||
"success_rate": 0.6,
|
||||
"cooldown": 600, # 10 minutes
|
||||
"jail_time": 3600, # 1 hour if caught
|
||||
"risk": "low",
|
||||
"enabled": True,
|
||||
"fine_multiplier": 0.35, # 35% of max reward as fine
|
||||
"min_steal_percentage": 0.01, # Steal 1-10% of target's credits
|
||||
"max_steal_percentage": 0.10
|
||||
},
|
||||
"mugging": {
|
||||
"requires_target": True,
|
||||
"min_reward": 400,
|
||||
"max_reward": 1500,
|
||||
"success_rate": 0.6,
|
||||
"cooldown": 1800, # 30 minutes
|
||||
"jail_time": 5400, # 1 hour 30 minutes if caught
|
||||
"risk": "medium",
|
||||
"enabled": True,
|
||||
"fine_multiplier": 0.4, # 40% of max reward as fine
|
||||
"min_steal_percentage": 0.15, # Steal 15-25% of target's credits
|
||||
"max_steal_percentage": 0.25
|
||||
},
|
||||
"rob_store": {
|
||||
"requires_target": False,
|
||||
"min_reward": 500,
|
||||
"max_reward": 2000,
|
||||
"success_rate": 0.5,
|
||||
"cooldown": 21600, # 6 hours
|
||||
"jail_time": 10800, # 3 hours if caught
|
||||
"risk": "medium",
|
||||
"enabled": True,
|
||||
"fine_multiplier": 0.4, # 45% of max reward as fine
|
||||
"steal_percentage": 0
|
||||
},
|
||||
"bank_heist": {
|
||||
"requires_target": False,
|
||||
"min_reward": 1500,
|
||||
"max_reward": 5000,
|
||||
"success_rate": 0.4,
|
||||
"cooldown": 86400, # 1 day
|
||||
"jail_time": 14400, # 4 hours if caught
|
||||
"risk": "high",
|
||||
"enabled": True,
|
||||
"fine_multiplier": 0.4, # 40% of max reward as fine
|
||||
"steal_percentage": 0
|
||||
},
|
||||
"random": {
|
||||
"requires_target": False,
|
||||
"min_reward": 100, # Will be overridden by scenario
|
||||
"max_reward": 3000, # Will be overridden by scenario
|
||||
"success_rate": 0.5, # Will be overridden by scenario
|
||||
"cooldown": 3600, # 1 hour
|
||||
"jail_time": 600, # Will be overridden by scenario
|
||||
"risk": "random", # Will be determined by scenario
|
||||
"enabled": True,
|
||||
"fine_multiplier": 0.5, # Will be overridden by scenario
|
||||
"steal_percentage": 0
|
||||
}
|
||||
}
|
||||
|
||||
# Default guild settings
|
||||
DEFAULT_GUILD = {
|
||||
"crime_options": CRIME_TYPES,
|
||||
"global_settings": {
|
||||
"allow_bail": True,
|
||||
"bail_cost_multiplier": 1.6,
|
||||
"min_steal_balance": 100,
|
||||
"max_steal_amount": 1000,
|
||||
"default_jail_time": 1800, # 30 minutes
|
||||
"default_fine_multiplier": 0.5,
|
||||
"protect_low_balance": True, # Prevent stealing from users with very low balance
|
||||
"show_success_rate": True, # Show success rate in crime messages
|
||||
"show_fine_amount": True, # Show potential fine amounts
|
||||
"enable_random_events": True # Enable random events during crimes
|
||||
},
|
||||
"custom_scenarios": [] # List to store custom scenarios for this guild
|
||||
}
|
||||
|
||||
# Default member settings specific to crime
|
||||
DEFAULT_MEMBER = {
|
||||
"jail_time": 0, # Total time to serve
|
||||
"jail_started": 0, # When jail time started
|
||||
"attempted_jailbreak": False, # Whether attempted jailbreak this sentence
|
||||
"cooldowns": {}, # Crime cooldowns
|
||||
"total_successful_crimes": 0,
|
||||
"total_failed_crimes": 0,
|
||||
"total_fines_paid": 0,
|
||||
"total_credits_earned": 0,
|
||||
"largest_heist": 0, # Track largest successful heist
|
||||
"total_stolen_from": 0, # Amount stolen from others
|
||||
"total_stolen_by": 0, # Amount stolen by others
|
||||
"total_bail_paid": 0, # Amount spent on bail
|
||||
"notify_on_release": False, # Whether to ping user when jail sentence is over
|
||||
"current_streak": 0, # Current successful crime streak
|
||||
"highest_streak": 0, # Highest streak achieved
|
||||
"streak_multiplier": 1.0, # Current reward multiplier from streak
|
||||
}
|
547
city/crime/jail.py
Normal file
547
city/crime/jail.py
Normal file
|
@ -0,0 +1,547 @@
|
|||
"""Jail management system for the City cog.
|
||||
|
||||
|
||||
CURRENTLY NOT IN USE AND WIP
|
||||
|
||||
"""
|
||||
from typing import Optional, Dict, Any, Tuple, List
|
||||
import time
|
||||
import asyncio
|
||||
import random
|
||||
import discord
|
||||
from redbot.core import Config, bank
|
||||
from redbot.core.bot import Red
|
||||
from redbot.core.utils.chat_formatting import humanize_number
|
||||
from redbot.core.utils.predicates import MessagePredicate
|
||||
from redbot.core.i18n import Translator
|
||||
from .scenarios import get_random_jailbreak_scenario
|
||||
|
||||
_ = Translator("City", __file__)
|
||||
|
||||
class JailException(Exception):
|
||||
"""Base exception for jail-related errors."""
|
||||
pass
|
||||
|
||||
class JailStateError(JailException):
|
||||
"""Raised when there's an error with jail state management."""
|
||||
pass
|
||||
|
||||
class JailNotificationError(JailException):
|
||||
"""Raised when there's an error with jail notifications."""
|
||||
pass
|
||||
|
||||
class BailError(JailException):
|
||||
"""Raised when there's an error with bail operations."""
|
||||
pass
|
||||
|
||||
class JailbreakError(JailException):
|
||||
"""Raised when there's an error with jailbreak operations."""
|
||||
pass
|
||||
|
||||
class PerkError(JailException):
|
||||
"""Raised when there's an error with perk operations."""
|
||||
pass
|
||||
|
||||
class PerkManager:
|
||||
"""Manages jail-related perks."""
|
||||
|
||||
def __init__(self, config: Config):
|
||||
self.config = config
|
||||
|
||||
async def has_perk(self, member: discord.Member, perk_name: str) -> bool:
|
||||
"""Check if a member has a specific perk."""
|
||||
try:
|
||||
member_data = await self.config.member(member).all()
|
||||
return perk_name in member_data.get("purchased_perks", [])
|
||||
except Exception as e:
|
||||
raise PerkError(f"Failed to check perk status: {str(e)}")
|
||||
|
||||
async def apply_sentence_reduction(self, member: discord.Member, jail_time: int) -> int:
|
||||
"""Apply sentence reduction if member has the jail_reducer perk."""
|
||||
try:
|
||||
if await self.has_perk(member, "jail_reducer"):
|
||||
return int(jail_time * 0.8) # 20% reduction
|
||||
return jail_time
|
||||
except Exception as e:
|
||||
raise PerkError(f"Failed to apply sentence reduction: {str(e)}")
|
||||
|
||||
async def should_notify(self, member: discord.Member) -> bool:
|
||||
"""Check if member should receive jail notifications."""
|
||||
try:
|
||||
member_data = await self.config.member(member).all()
|
||||
has_notifier = await self.has_perk(member, "jail_notifier")
|
||||
notify_enabled = member_data.get("notify_on_release", False)
|
||||
return has_notifier and notify_enabled
|
||||
except Exception as e:
|
||||
raise PerkError(f"Failed to check notification status: {str(e)}")
|
||||
|
||||
async def toggle_notifications(self, member: discord.Member) -> bool:
|
||||
"""Toggle jail release notifications for a member."""
|
||||
try:
|
||||
if not await self.has_perk(member, "jail_notifier"):
|
||||
raise PerkError("You don't have the jail notifier perk!")
|
||||
|
||||
async with self.config.member(member).all() as member_data:
|
||||
current = member_data.get("notify_on_release", False)
|
||||
member_data["notify_on_release"] = not current
|
||||
return not current
|
||||
except Exception as e:
|
||||
raise PerkError(f"Failed to toggle notifications: {str(e)}")
|
||||
|
||||
class JailbreakScenario:
|
||||
"""Represents a jailbreak attempt scenario."""
|
||||
|
||||
def __init__(self, data: Dict[str, Any]):
|
||||
self.name = data["name"]
|
||||
self.attempt_text = data["attempt_text"]
|
||||
self.success_text = data["success_text"]
|
||||
self.fail_text = data["fail_text"]
|
||||
self.base_chance = data["base_chance"]
|
||||
self.events = data["events"]
|
||||
|
||||
@property
|
||||
def random_events(self) -> List[Dict[str, Any]]:
|
||||
"""Get 1-3 random events for this scenario."""
|
||||
num_events = random.randint(1, 3)
|
||||
return random.sample(self.events, num_events)
|
||||
|
||||
def format_text(self, text: str, **kwargs) -> str:
|
||||
"""Format scenario text with given parameters."""
|
||||
return _(text).format(**kwargs)
|
||||
|
||||
class JailManager:
|
||||
"""Manages all jail-related functionality."""
|
||||
|
||||
def __init__(self, bot: Red, config: Config):
|
||||
self.bot = bot
|
||||
self.config = config
|
||||
self.notification_tasks: Dict[int, asyncio.Task] = {} # member_id: task
|
||||
self.perk_manager = PerkManager(config)
|
||||
|
||||
async def get_jail_state(self, member: discord.Member) -> Dict[str, Any]:
|
||||
"""Get the complete jail state for a member."""
|
||||
try:
|
||||
member_data = await self.config.member(member).all()
|
||||
return {
|
||||
"jail_until": member_data.get("jail_until", 0),
|
||||
"attempted_jailbreak": member_data.get("attempted_jailbreak", False),
|
||||
"jail_channel": member_data.get("jail_channel", None),
|
||||
"notify_on_release": member_data.get("notify_on_release", False),
|
||||
"reduced_sentence": "jail_reducer" in member_data.get("purchased_perks", []),
|
||||
"original_sentence": member_data.get("original_sentence", 0)
|
||||
}
|
||||
except Exception as e:
|
||||
raise JailStateError(f"Failed to get jail state: {str(e)}")
|
||||
|
||||
async def update_jail_state(self, member: discord.Member, state: Dict[str, Any]) -> None:
|
||||
"""Update the jail state for a member."""
|
||||
try:
|
||||
async with self.config.member(member).all() as member_data:
|
||||
member_data["jail_until"] = state.get("jail_until", member_data.get("jail_until", 0))
|
||||
member_data["attempted_jailbreak"] = state.get("attempted_jailbreak", member_data.get("attempted_jailbreak", False))
|
||||
member_data["jail_channel"] = state.get("jail_channel", member_data.get("jail_channel", None))
|
||||
member_data["notify_on_release"] = state.get("notify_on_release", member_data.get("notify_on_release", False))
|
||||
member_data["original_sentence"] = state.get("original_sentence", member_data.get("original_sentence", 0))
|
||||
except Exception as e:
|
||||
raise JailStateError(f"Failed to update jail state: {str(e)}")
|
||||
|
||||
async def get_jail_time_remaining(self, member: discord.Member) -> int:
|
||||
"""Get remaining jail time in seconds."""
|
||||
try:
|
||||
state = await self.get_jail_state(member)
|
||||
jail_until = state["jail_until"]
|
||||
|
||||
if not jail_until:
|
||||
return 0
|
||||
|
||||
current_time = int(time.time())
|
||||
remaining = max(0, jail_until - current_time)
|
||||
|
||||
# Clear jail if time is up
|
||||
if remaining == 0 and jail_until != 0:
|
||||
await self.clear_jail_state(member)
|
||||
|
||||
return remaining
|
||||
except Exception as e:
|
||||
raise JailStateError(f"Failed to get remaining jail time: {str(e)}")
|
||||
|
||||
async def send_to_jail(self, member: discord.Member, jail_time: int, channel: Optional[discord.TextChannel] = None) -> None:
|
||||
"""Send a member to jail."""
|
||||
try:
|
||||
# Get current state
|
||||
state = await self.get_jail_state(member)
|
||||
original_time = jail_time
|
||||
|
||||
# Apply sentence reduction if they have the perk
|
||||
jail_time = await self.perk_manager.apply_sentence_reduction(member, jail_time)
|
||||
|
||||
# Update state
|
||||
new_state = {
|
||||
"jail_until": int(time.time()) + jail_time,
|
||||
"attempted_jailbreak": False, # Reset on new sentence
|
||||
"jail_channel": channel.id if channel else None,
|
||||
"original_sentence": original_time
|
||||
}
|
||||
await self.update_jail_state(member, new_state)
|
||||
|
||||
# Handle notification scheduling
|
||||
if await self.perk_manager.should_notify(member):
|
||||
await self._schedule_release_notification(member, jail_time, channel)
|
||||
|
||||
except Exception as e:
|
||||
raise JailStateError(f"Failed to send member to jail: {str(e)}")
|
||||
|
||||
async def clear_jail_state(self, member: discord.Member) -> None:
|
||||
"""Clear all jail-related state for a member."""
|
||||
try:
|
||||
# Cancel any pending notifications
|
||||
await self._cancel_notification(member)
|
||||
|
||||
# Clear jail state
|
||||
await self.update_jail_state(member, {
|
||||
"jail_until": 0,
|
||||
"attempted_jailbreak": False,
|
||||
"jail_channel": None,
|
||||
"original_sentence": 0
|
||||
})
|
||||
except Exception as e:
|
||||
raise JailStateError(f"Failed to clear jail state: {str(e)}")
|
||||
|
||||
async def _schedule_release_notification(self, member: discord.Member, jail_time: int, channel: Optional[discord.TextChannel] = None) -> None:
|
||||
"""Schedule a notification for when a member's jail sentence is over."""
|
||||
try:
|
||||
# Cancel any existing notification
|
||||
await self._cancel_notification(member)
|
||||
|
||||
# Create new notification task
|
||||
task = asyncio.create_task(self._notification_handler(member, jail_time, channel))
|
||||
self.notification_tasks[member.id] = task
|
||||
|
||||
except Exception as e:
|
||||
raise JailNotificationError(f"Failed to schedule release notification: {str(e)}")
|
||||
|
||||
async def _cancel_notification(self, member: discord.Member) -> None:
|
||||
"""Cancel a pending release notification."""
|
||||
try:
|
||||
if member.id in self.notification_tasks:
|
||||
self.notification_tasks[member.id].cancel()
|
||||
del self.notification_tasks[member.id]
|
||||
except Exception as e:
|
||||
raise JailNotificationError(f"Failed to cancel notification: {str(e)}")
|
||||
|
||||
async def _notification_handler(self, member: discord.Member, jail_time: int, channel: Optional[discord.TextChannel] = None) -> None:
|
||||
"""Handle the notification process for jail release."""
|
||||
try:
|
||||
await asyncio.sleep(jail_time)
|
||||
|
||||
# Verify they're actually out (in case sentence was extended)
|
||||
remaining = await self.get_jail_time_remaining(member)
|
||||
if remaining <= 0:
|
||||
state = await self.get_jail_state(member)
|
||||
if state["notify_on_release"]:
|
||||
await self._send_notification(member, channel)
|
||||
except asyncio.CancelledError:
|
||||
pass # Task was cancelled, ignore
|
||||
except Exception as e:
|
||||
raise JailNotificationError(f"Notification handler failed: {str(e)}")
|
||||
|
||||
async def _send_notification(self, member: discord.Member, channel: Optional[discord.TextChannel] = None) -> None:
|
||||
"""Send the actual notification message."""
|
||||
try:
|
||||
message = f"🔔 {member.mention} Your jail sentence is over! You're now free to commit crimes again."
|
||||
|
||||
if channel:
|
||||
await channel.send(message)
|
||||
else:
|
||||
try:
|
||||
await member.send(message)
|
||||
except (discord.Forbidden, discord.HTTPException):
|
||||
pass # Can't DM the user, silently fail
|
||||
except Exception as e:
|
||||
raise JailNotificationError(f"Failed to send notification: {str(e)}")
|
||||
|
||||
def is_in_jail(self, member: discord.Member) -> bool:
|
||||
"""Quick check if a member is currently in jail."""
|
||||
return self.get_jail_time_remaining(member) > 0
|
||||
|
||||
async def calculate_bail_cost(self, member: discord.Member) -> Tuple[int, float]:
|
||||
"""Calculate bail cost based on remaining jail time.
|
||||
|
||||
Returns:
|
||||
Tuple[int, float]: (bail_cost, multiplier)
|
||||
"""
|
||||
try:
|
||||
# Get remaining time and settings
|
||||
remaining_time = await self.get_jail_time_remaining(member)
|
||||
if remaining_time <= 0:
|
||||
raise BailError("Member is not in jail")
|
||||
|
||||
settings = await self.config.guild(member.guild).global_settings()
|
||||
multiplier = float(settings.get("bail_cost_multiplier", 1.5))
|
||||
|
||||
# Calculate cost (remaining minutes * multiplier)
|
||||
bail_cost = int(multiplier * (remaining_time / 60)) # Convert seconds to minutes
|
||||
|
||||
return bail_cost, multiplier
|
||||
|
||||
except Exception as e:
|
||||
raise BailError(f"Failed to calculate bail cost: {str(e)}")
|
||||
|
||||
async def can_pay_bail(self, member: discord.Member) -> Tuple[bool, int, str]:
|
||||
"""Check if a member can pay bail.
|
||||
|
||||
Returns:
|
||||
Tuple[bool, int, str]: (can_pay, cost, currency_name)
|
||||
"""
|
||||
try:
|
||||
# Check if bail is allowed
|
||||
settings = await self.config.guild(member.guild).global_settings()
|
||||
if not settings.get("allow_bail", True):
|
||||
raise BailError("Bail is not allowed in this server")
|
||||
|
||||
# Calculate bail cost
|
||||
bail_cost, _ = await self.calculate_bail_cost(member)
|
||||
|
||||
# Get currency info
|
||||
currency_name = await bank.get_currency_name(member.guild)
|
||||
can_pay = await bank.can_spend(member, bail_cost)
|
||||
|
||||
return can_pay, bail_cost, currency_name
|
||||
|
||||
except Exception as e:
|
||||
raise BailError(f"Failed to check bail payment: {str(e)}")
|
||||
|
||||
async def process_bail_payment(self, member: discord.Member) -> Tuple[int, int, str]:
|
||||
"""Process bail payment and release from jail.
|
||||
|
||||
Returns:
|
||||
Tuple[int, int, str]: (bail_cost, new_balance, currency_name)
|
||||
"""
|
||||
try:
|
||||
# Verify they can pay
|
||||
can_pay, bail_cost, currency_name = await self.can_pay_bail(member)
|
||||
if not can_pay:
|
||||
raise BailError(f"Insufficient funds to pay {bail_cost} {currency_name}")
|
||||
|
||||
# Process payment
|
||||
await bank.withdraw_credits(member, bail_cost)
|
||||
new_balance = await bank.get_balance(member)
|
||||
|
||||
# Update stats
|
||||
async with self.config.member(member).all() as member_data:
|
||||
member_data["total_bail_paid"] = member_data.get("total_bail_paid", 0) + bail_cost
|
||||
|
||||
# Release from jail
|
||||
await self.clear_jail_state(member)
|
||||
|
||||
return bail_cost, new_balance, currency_name
|
||||
|
||||
except Exception as e:
|
||||
raise BailError(f"Failed to process bail payment: {str(e)}")
|
||||
|
||||
async def format_bail_embed(self, member: discord.Member) -> discord.Embed:
|
||||
"""Format bail information into an embed."""
|
||||
try:
|
||||
# Get bail information
|
||||
remaining_time = await self.get_jail_time_remaining(member)
|
||||
bail_cost, multiplier = await self.calculate_bail_cost(member)
|
||||
current_balance = await bank.get_balance(member)
|
||||
currency_name = await bank.get_currency_name(member.guild)
|
||||
|
||||
# Get member data for perk check
|
||||
state = await self.get_jail_state(member)
|
||||
|
||||
# Create embed
|
||||
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:** {self._format_time(remaining_time)}"
|
||||
+ (" (Reduced by 20%)" if state["reduced_sentence"] else "") + "\n"
|
||||
f"**Bail Cost:** {bail_cost:,} {currency_name}\n"
|
||||
f"**Current Balance:** {current_balance:,} {currency_name}\n\n"
|
||||
f"*Bail cost is calculated as: remaining minutes × {multiplier}*"
|
||||
),
|
||||
color=discord.Color.gold(),
|
||||
timestamp=discord.utils.utcnow()
|
||||
)
|
||||
embed.set_footer(text=f"Requested by {member.display_name}", icon_url=member.display_avatar.url)
|
||||
|
||||
return embed
|
||||
|
||||
except Exception as e:
|
||||
raise BailError(f"Failed to format bail embed: {str(e)}")
|
||||
|
||||
def _format_time(self, seconds: int) -> str:
|
||||
"""Format seconds into a human-readable string."""
|
||||
minutes = seconds // 60
|
||||
remaining_seconds = seconds % 60
|
||||
return f"{minutes}m {remaining_seconds}s"
|
||||
|
||||
async def get_jailbreak_scenario(self) -> JailbreakScenario:
|
||||
"""Get a random jailbreak scenario."""
|
||||
try:
|
||||
# Get scenario from scenarios.py
|
||||
scenario_data = get_random_jailbreak_scenario()
|
||||
return JailbreakScenario(scenario_data)
|
||||
except Exception as e:
|
||||
raise JailbreakError(f"Failed to get jailbreak scenario: {str(e)}")
|
||||
|
||||
async def process_jailbreak_attempt(self, member: discord.Member, channel: discord.TextChannel) -> Tuple[bool, discord.Embed, List[str]]:
|
||||
"""Process a jailbreak attempt.
|
||||
|
||||
Returns:
|
||||
Tuple[bool, discord.Embed, List[str]]: (success, result_embed, event_messages)
|
||||
"""
|
||||
try:
|
||||
# Verify member is in jail
|
||||
if not await self.is_in_jail(member):
|
||||
raise JailbreakError("Member is not in jail")
|
||||
|
||||
# Check if already attempted
|
||||
state = await self.get_jail_state(member)
|
||||
if state["attempted_jailbreak"]:
|
||||
raise JailbreakError("Already attempted jailbreak this sentence")
|
||||
|
||||
# Mark attempt
|
||||
await self.update_jail_state(member, {"attempted_jailbreak": True})
|
||||
|
||||
# Get scenario and process events
|
||||
scenario = await self.get_jailbreak_scenario()
|
||||
success_chance = scenario.base_chance
|
||||
event_messages = []
|
||||
|
||||
# Process random events
|
||||
for event in scenario.random_events:
|
||||
event_text = event["text"]
|
||||
currency_name = await bank.get_currency_name(member.guild)
|
||||
event_text = event_text.format(currency=currency_name)
|
||||
|
||||
# Apply chance 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"])
|
||||
|
||||
# Apply currency effects
|
||||
if "currency_bonus" in event:
|
||||
await bank.deposit_credits(member, event["currency_bonus"])
|
||||
event_text += f" (+{event['currency_bonus']} {currency_name})"
|
||||
elif "currency_penalty" in event:
|
||||
await bank.withdraw_credits(member, event["currency_penalty"])
|
||||
event_text += f" (-{event['currency_penalty']} {currency_name})"
|
||||
|
||||
event_messages.append(event_text)
|
||||
|
||||
# Roll for success
|
||||
success = random.random() < success_chance
|
||||
|
||||
if success:
|
||||
# Clear jail time
|
||||
await self.clear_jail_state(member)
|
||||
embed = discord.Embed(
|
||||
title="🔓 Successful Jailbreak!",
|
||||
description=scenario.format_text(scenario.success_text, user=member.mention),
|
||||
color=discord.Color.green()
|
||||
)
|
||||
else:
|
||||
# Add 30% more time
|
||||
remaining_time = await self.get_jail_time_remaining(member)
|
||||
added_time = int(remaining_time * 0.3)
|
||||
new_until = int(time.time()) + remaining_time + added_time
|
||||
await self.update_jail_state(member, {"jail_until": new_until})
|
||||
|
||||
# Create fail embed
|
||||
embed = discord.Embed(
|
||||
title="⛓️ Failed Jailbreak!",
|
||||
description=scenario.format_text(scenario.fail_text, user=member.mention),
|
||||
color=discord.Color.red()
|
||||
)
|
||||
|
||||
# Add penalty info
|
||||
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
|
||||
)
|
||||
|
||||
# Add chance field
|
||||
embed.add_field(
|
||||
name="🎲 Final Escape Chance",
|
||||
value=f"{success_chance:.1%}",
|
||||
inline=True
|
||||
)
|
||||
|
||||
return success, embed, event_messages
|
||||
|
||||
except Exception as e:
|
||||
raise JailbreakError(f"Failed to process jailbreak attempt: {str(e)}")
|
||||
|
||||
async def format_jailbreak_embed(self, member: discord.Member, scenario: JailbreakScenario) -> discord.Embed:
|
||||
"""Format the initial jailbreak attempt embed."""
|
||||
try:
|
||||
embed = discord.Embed(
|
||||
title="🔓 Jailbreak Attempt",
|
||||
description=scenario.format_text(scenario.attempt_text, user=member.mention),
|
||||
color=discord.Color.gold()
|
||||
)
|
||||
embed.add_field(
|
||||
name="📊 Base Success Chance",
|
||||
value=f"{scenario.base_chance:.1%}",
|
||||
inline=True
|
||||
)
|
||||
return embed
|
||||
except Exception as e:
|
||||
raise JailbreakError(f"Failed to format jailbreak embed: {str(e)}")
|
||||
|
||||
async def format_jail_status(self, member: discord.Member) -> discord.Embed:
|
||||
"""Format jail status into an embed."""
|
||||
try:
|
||||
state = await self.get_jail_state(member)
|
||||
remaining_time = await self.get_jail_time_remaining(member)
|
||||
|
||||
if remaining_time <= 0:
|
||||
return discord.Embed(
|
||||
title="🆓 Not in Jail",
|
||||
description=f"{member.mention} is not currently in jail.",
|
||||
color=discord.Color.green()
|
||||
)
|
||||
|
||||
embed = discord.Embed(
|
||||
title="⛓️ Jail Status",
|
||||
description=f"{member.mention} is currently in jail!",
|
||||
color=discord.Color.red()
|
||||
)
|
||||
|
||||
# Add time info
|
||||
minutes = remaining_time // 60
|
||||
seconds = remaining_time % 60
|
||||
embed.add_field(
|
||||
name="⏰ Time Remaining",
|
||||
value=f"{minutes}m {seconds}s" + (" (Reduced by 20%)" if state["reduced_sentence"] else ""),
|
||||
inline=True
|
||||
)
|
||||
|
||||
# Add notification status if they have the perk
|
||||
if await self.perk_manager.has_perk(member, "jail_notifier"):
|
||||
embed.add_field(
|
||||
name="🔔 Notifications",
|
||||
value="Enabled" if state["notify_on_release"] else "Disabled",
|
||||
inline=True
|
||||
)
|
||||
|
||||
# Add jailbreak attempt status
|
||||
embed.add_field(
|
||||
name="🔓 Jailbreak Attempt",
|
||||
value="Already Attempted" if state["attempted_jailbreak"] else "Not Attempted",
|
||||
inline=True
|
||||
)
|
||||
|
||||
return embed
|
||||
|
||||
except Exception as e:
|
||||
raise JailStateError(f"Failed to format jail status: {str(e)}")
|
1350
city/crime/scenarios.py
Normal file
1350
city/crime/scenarios.py
Normal file
File diff suppressed because it is too large
Load diff
2055
city/crime/views.py
Normal file
2055
city/crime/views.py
Normal file
File diff suppressed because it is too large
Load diff
16
city/info.json
Normal file
16
city/info.json
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"name": "City",
|
||||
"short": "A comprehensive city simulation system with various activities",
|
||||
"description": "A modular city simulation system that includes various activities for users to engage in. Currently features a crime system for earning credits through virtual crimes with different risk levels, cooldowns, rewards, jail system, jail breaks, dynamic events, and more.",
|
||||
"end_user_data_statement": "This cog stores user data including:\n- Cooldown timers for various activities\n- Statistics for activities (e.g., successful/failed crimes)\n- Jail status and duration\n- Last target information for anti-farming\nAll data can be removed with the standard Red delete data commands.",
|
||||
"install_msg": "Thanks for installing the City cog!\n\nNotes:\n- Uses Config to store user stats and settings\n- Moderate memory usage for tracking active crimes and jail status\n- Requires bank to be registered (`[p]bank register`)\n- Requires manage_messages permission for button interactions\n- Anti-farming measures track last target IDs\n\nGet started with `[p]crime` to explore the crime system. Use `[p]crimeset` for configuration.",
|
||||
"author": ["CalaMariGold"],
|
||||
"required_cogs": {},
|
||||
"requirements": [],
|
||||
"tags": ["fun", "economy", "games", "crime", "city", "simulation", "interactive"],
|
||||
"min_bot_version": "3.5.0",
|
||||
"min_python_version": [3, 8, 1],
|
||||
"hidden": false,
|
||||
"disabled": false,
|
||||
"type": "COG"
|
||||
}
|
567
city/inventory.py
Normal file
567
city/inventory.py
Normal file
|
@ -0,0 +1,567 @@
|
|||
"""Inventory system for the City cog.
|
||||
|
||||
This module provides a flexible inventory system that can be used by any module
|
||||
in the City cog. It supports both consumable and permanent items, with features
|
||||
for activating and selling items.
|
||||
|
||||
The system is designed to be extensible, allowing different modules to register
|
||||
their own items with custom activation and sale behaviors.
|
||||
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Optional, Union, Any, Tuple, Callable
|
||||
import discord
|
||||
from redbot.core import bank, commands
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
import asyncio
|
||||
|
||||
async def cleanup_inventory(member_data: Dict[str, Any], item_registry: Dict[str, Dict[str, Any]]) -> Dict[str, Any]:
|
||||
"""Clean up expired or invalid items from member data.
|
||||
|
||||
Args:
|
||||
member_data (Dict[str, Any]): The member's data containing inventory and perks
|
||||
item_registry (Dict[str, Dict[str, Any]]): Registry of available items
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The cleaned member data
|
||||
"""
|
||||
current_time = int(time.time())
|
||||
|
||||
# Clean up active items
|
||||
if "active_items" in member_data:
|
||||
active_items = member_data["active_items"]
|
||||
to_remove = []
|
||||
|
||||
for item_id, status in active_items.items():
|
||||
# Remove if item no longer exists in registry
|
||||
if item_id not in item_registry:
|
||||
to_remove.append(item_id)
|
||||
continue
|
||||
|
||||
item = item_registry[item_id]
|
||||
if "duration" in item:
|
||||
# Remove if duration expired
|
||||
end_time = status.get("end_time", 0)
|
||||
if end_time <= current_time:
|
||||
to_remove.append(item_id)
|
||||
elif "uses" in status:
|
||||
# Remove if no uses left
|
||||
if status["uses"] <= 0:
|
||||
to_remove.append(item_id)
|
||||
|
||||
for item_id in to_remove:
|
||||
del active_items[item_id]
|
||||
|
||||
# Clean up purchased perks
|
||||
if "purchased_perks" in member_data:
|
||||
# Remove perks that no longer exist in registry
|
||||
member_data["purchased_perks"] = [
|
||||
perk_id for perk_id in member_data["purchased_perks"]
|
||||
if perk_id in item_registry
|
||||
]
|
||||
|
||||
return member_data
|
||||
|
||||
class InventoryView(discord.ui.View):
|
||||
"""A view for displaying and managing a user's inventory.
|
||||
|
||||
This view provides functionality for activating and selling items from the user's inventory.
|
||||
It supports both consumable and permanent items, with appropriate handling for each type.
|
||||
|
||||
Attributes:
|
||||
ctx (commands.Context): The command context
|
||||
cog (commands.Cog): The cog instance that created this view
|
||||
member_data (Dict[str, Any]): The member's data containing inventory and perks
|
||||
item_registry (Dict[str, Dict[str, Any]]): Registry of available items
|
||||
message (Optional[discord.Message]): The message containing this view
|
||||
"""
|
||||
|
||||
def __init__(self, ctx: commands.Context, cog: commands.Cog, member_data: Dict[str, Any], item_registry: Dict[str, Dict[str, Any]]) -> None:
|
||||
super().__init__(timeout=180)
|
||||
self.ctx = ctx
|
||||
self.cog = cog
|
||||
self.member_data = member_data
|
||||
self.item_registry = item_registry
|
||||
self.message: Optional[discord.Message] = None
|
||||
|
||||
async def _update_options(self) -> None:
|
||||
"""Update the select menu options based on the user's inventory."""
|
||||
# Clear existing options
|
||||
for child in self.children[:]:
|
||||
self.remove_item(child)
|
||||
|
||||
current_time = int(time.time())
|
||||
owned_items = []
|
||||
currency_name = await bank.get_currency_name(self.ctx.guild)
|
||||
|
||||
# Add owned perks
|
||||
for perk_id in self.member_data.get("purchased_perks", []):
|
||||
if perk_id in self.item_registry:
|
||||
owned_items.append((perk_id, self.item_registry[perk_id], None))
|
||||
|
||||
# Add active items
|
||||
for item_id, status in self.member_data.get("active_items", {}).items():
|
||||
if item_id not in self.item_registry:
|
||||
continue
|
||||
|
||||
item = self.item_registry[item_id]
|
||||
if "duration" in item:
|
||||
end_time = status.get("end_time", 0)
|
||||
if end_time > current_time:
|
||||
remaining = end_time - current_time
|
||||
owned_items.append((item_id, item, f"{remaining // 3600}h {(remaining % 3600) // 60}m remaining"))
|
||||
elif "uses" in status:
|
||||
uses = status.get("uses", 0)
|
||||
if uses > 0:
|
||||
owned_items.append((item_id, item, f"{uses} uses remaining"))
|
||||
|
||||
if not owned_items:
|
||||
# Add placeholder if no items
|
||||
self.add_item(discord.ui.Select(
|
||||
placeholder="No items in inventory",
|
||||
options=[discord.SelectOption(label="Empty", value="empty", description="Your inventory is empty")],
|
||||
disabled=True
|
||||
))
|
||||
return
|
||||
|
||||
# Add activation select
|
||||
activate_options = []
|
||||
for item_id, item, status in owned_items:
|
||||
# Skip perks that aren't toggleable
|
||||
if item["type"] == "perk" and not item.get("toggleable", False):
|
||||
continue
|
||||
|
||||
description = item["description"]
|
||||
if status:
|
||||
description = f"{status} - {description}"
|
||||
|
||||
if item_id == "notify_ping":
|
||||
current_status = self.member_data.get("notify_on_release", False)
|
||||
label = f"{item['name']} ({'Enabled' if current_status else 'Disabled'})"
|
||||
else:
|
||||
label = item['name']
|
||||
|
||||
activate_options.append(
|
||||
discord.SelectOption(
|
||||
label=label,
|
||||
value=f"activate_{item_id}",
|
||||
description=description,
|
||||
emoji=item.get("emoji", "📦")
|
||||
)
|
||||
)
|
||||
|
||||
if activate_options:
|
||||
activate_select = discord.ui.Select(
|
||||
placeholder="Select an item to use/toggle",
|
||||
options=activate_options,
|
||||
row=0
|
||||
)
|
||||
activate_select.callback = self._handle_activation
|
||||
self.add_item(activate_select)
|
||||
|
||||
# Add sell select for sellable items
|
||||
sell_options = []
|
||||
for item_id, item, status in owned_items:
|
||||
if not item.get("can_sell", True):
|
||||
continue
|
||||
|
||||
sell_price = int(item["cost"] * (0.25 if item["type"] == "perk" else 0.5))
|
||||
|
||||
description = f"Sell for {sell_price:,} {currency_name}"
|
||||
if status:
|
||||
description = f"{status} - {description}"
|
||||
|
||||
sell_options.append(
|
||||
discord.SelectOption(
|
||||
label=f"Sell {item['name']}",
|
||||
value=f"sell_{item_id}",
|
||||
description=description,
|
||||
emoji="💰"
|
||||
)
|
||||
)
|
||||
|
||||
if sell_options:
|
||||
sell_select = discord.ui.Select(
|
||||
placeholder="Select an item to sell",
|
||||
options=sell_options,
|
||||
row=1
|
||||
)
|
||||
sell_select.callback = self._handle_sale
|
||||
self.add_item(sell_select)
|
||||
|
||||
async def _create_embed(self) -> discord.Embed:
|
||||
"""Create the inventory embed.
|
||||
|
||||
Returns:
|
||||
discord.Embed: The created embed
|
||||
"""
|
||||
embed = discord.Embed(
|
||||
title="🎒 Your Inventory",
|
||||
description="Select an item to activate or sell it.",
|
||||
color=discord.Color.blue()
|
||||
)
|
||||
|
||||
# Add perks section
|
||||
perks = []
|
||||
for perk_id in self.member_data.get("purchased_perks", []):
|
||||
if perk_id in self.item_registry:
|
||||
perk = self.item_registry[perk_id]
|
||||
status = ""
|
||||
if perk_id == "notify_ping":
|
||||
status = " (Enabled)" if self.member_data.get("notify_on_release", False) else " (Disabled)"
|
||||
perks.append(f"{perk['emoji']} **{perk['name']}**{status}\n↳ {perk['description']}")
|
||||
|
||||
if perks:
|
||||
embed.add_field(
|
||||
name="__🔒 Permanent Perks__",
|
||||
value="\n".join(perks),
|
||||
inline=False
|
||||
)
|
||||
|
||||
# Add active items section
|
||||
current_time = int(time.time())
|
||||
active_items = []
|
||||
for item_id, status in self.member_data.get("active_items", {}).items():
|
||||
if item_id not in self.item_registry:
|
||||
continue
|
||||
|
||||
item = self.item_registry[item_id]
|
||||
if "duration" in item:
|
||||
end_time = status.get("end_time", 0)
|
||||
if end_time > current_time:
|
||||
remaining = end_time - current_time
|
||||
time_str = format_time_remaining(remaining)
|
||||
active_items.append(f"{item['emoji']} **{item['name']}**\n↳ Time remaining: {time_str}")
|
||||
else:
|
||||
uses = status.get("uses", 0)
|
||||
if uses > 0:
|
||||
active_items.append(f"{item['emoji']} **{item['name']}**\n↳ {uses} uses remaining")
|
||||
|
||||
if active_items:
|
||||
embed.add_field(
|
||||
name="__📦 Active Items__",
|
||||
value="\n".join(active_items),
|
||||
inline=False
|
||||
)
|
||||
|
||||
if not perks and not active_items:
|
||||
embed.description = "Your inventory is empty!"
|
||||
|
||||
return embed
|
||||
|
||||
async def _update_message(self) -> None:
|
||||
"""Update both the view and embed of the inventory message."""
|
||||
# Create tasks for parallel execution
|
||||
tasks = [
|
||||
self._create_embed(),
|
||||
self._update_options(),
|
||||
bank.get_currency_name(self.ctx.guild) # Get this once for the whole update
|
||||
]
|
||||
|
||||
# Wait for all tasks to complete
|
||||
embed, _, _ = await asyncio.gather(*tasks)
|
||||
|
||||
# Update message with new embed and view
|
||||
await self.message.edit(embed=embed, view=self)
|
||||
|
||||
async def _handle_activation(self, interaction: discord.Interaction) -> None:
|
||||
"""Handle item activation.
|
||||
|
||||
Args:
|
||||
interaction (discord.Interaction): The interaction that triggered this callback
|
||||
"""
|
||||
if interaction.user.id != self.ctx.author.id:
|
||||
await interaction.response.send_message("This menu is not for you!", ephemeral=True)
|
||||
return
|
||||
|
||||
try:
|
||||
item_id = interaction.data["values"][0].replace("activate_", "")
|
||||
item = self.item_registry[item_id]
|
||||
except (KeyError, AttributeError):
|
||||
await interaction.response.send_message(
|
||||
"❌ This item no longer exists!",
|
||||
ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
# Handle activation based on item type
|
||||
if item["type"] == "perk":
|
||||
if not item.get("toggleable", False):
|
||||
await interaction.response.send_message(
|
||||
"❌ This perk cannot be toggled!",
|
||||
ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
if item_id == "notify_ping":
|
||||
# Toggle notification status
|
||||
async with self.cog.config.member(self.ctx.author).all() as member_data:
|
||||
current_status = member_data.get("notify_on_release", False)
|
||||
member_data["notify_on_release"] = not current_status
|
||||
new_status = member_data["notify_on_release"]
|
||||
self.member_data = member_data
|
||||
|
||||
await interaction.response.send_message(
|
||||
f"🔔 Notifications are now {'enabled' if new_status else 'disabled'}",
|
||||
ephemeral=True
|
||||
)
|
||||
await self._update_message()
|
||||
else:
|
||||
await interaction.response.send_message(
|
||||
f"✨ Activated {item['emoji']} **{item['name']}**",
|
||||
ephemeral=True
|
||||
)
|
||||
await self._update_message()
|
||||
elif item["type"] == "consumable":
|
||||
async with self.cog.config.member(self.ctx.author).all() as member_data:
|
||||
# Clean up expired items first
|
||||
member_data = await cleanup_inventory(member_data, self.item_registry)
|
||||
|
||||
if "active_items" not in member_data:
|
||||
member_data["active_items"] = {}
|
||||
|
||||
# Handle jail pass
|
||||
if item_id == "jail_pass":
|
||||
# Check if user is in jail
|
||||
if not await self.cog.is_jailed(self.ctx.author):
|
||||
await interaction.response.send_message(
|
||||
"❌ You're not in jail! Save your jail pass for when you need it.",
|
||||
ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
# Use the jail pass
|
||||
if member_data["active_items"][item_id]["uses"] <= 1:
|
||||
del member_data["active_items"][item_id]
|
||||
else:
|
||||
member_data["active_items"][item_id]["uses"] -= 1
|
||||
|
||||
# Release from jail
|
||||
member_data["jail_until"] = 0
|
||||
member_data["jail_channel"] = None # Clear jail channel
|
||||
member_data["attempted_jailbreak"] = False # Reset jailbreak attempt
|
||||
|
||||
await interaction.response.send_message(
|
||||
"🔑 Used your Get Out of Jail Free card! You are now free.",
|
||||
ephemeral=True
|
||||
)
|
||||
|
||||
self.member_data = member_data
|
||||
await self._update_message()
|
||||
return
|
||||
|
||||
# Handle duration-based items
|
||||
if "duration" in item:
|
||||
current_time = int(time.time())
|
||||
|
||||
# Check if item is already active
|
||||
if item_id in member_data["active_items"]:
|
||||
existing_end = member_data["active_items"][item_id].get("end_time", current_time)
|
||||
if existing_end > current_time:
|
||||
remaining = existing_end - current_time
|
||||
time_str = format_time_remaining(remaining)
|
||||
await interaction.response.send_message(
|
||||
f"❌ This item is already active for {time_str}!",
|
||||
ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
# Activate item with new duration
|
||||
member_data["active_items"][item_id] = {"end_time": current_time + item["duration"]}
|
||||
time_str = format_time_remaining(item["duration"])
|
||||
|
||||
await interaction.response.send_message(
|
||||
f"✨ Activated {item['emoji']} **{item['name']}** for {time_str}",
|
||||
ephemeral=True
|
||||
)
|
||||
# Handle use-based items
|
||||
else:
|
||||
current_uses = member_data["active_items"].get(item_id, {}).get("uses", 0)
|
||||
if current_uses <= 0: # Item not active or no uses left
|
||||
if "uses" in item: # New item with default uses
|
||||
member_data["active_items"][item_id] = {"uses": item["uses"]}
|
||||
await interaction.response.send_message(
|
||||
f"✨ Added {item['emoji']} **{item['name']}** with {item['uses']} uses",
|
||||
ephemeral=True
|
||||
)
|
||||
self.member_data = member_data
|
||||
await self._update_message()
|
||||
return
|
||||
else:
|
||||
await interaction.response.send_message(
|
||||
"❌ This item has no uses remaining!",
|
||||
ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
# Use the item
|
||||
if current_uses <= 1:
|
||||
del member_data["active_items"][item_id]
|
||||
else:
|
||||
member_data["active_items"][item_id]["uses"] = current_uses - 1
|
||||
|
||||
await interaction.response.send_message(
|
||||
f"✨ Used {item['emoji']} **{item['name']}**" +
|
||||
(f" ({current_uses-1} uses remaining)" if current_uses > 1 else ""),
|
||||
ephemeral=True
|
||||
)
|
||||
|
||||
self.member_data = member_data
|
||||
await self._update_message()
|
||||
|
||||
async def _handle_sale(self, interaction: discord.Interaction) -> None:
|
||||
"""Handle item sale.
|
||||
|
||||
Args:
|
||||
interaction (discord.Interaction): The interaction that triggered this callback
|
||||
"""
|
||||
if interaction.user.id != self.ctx.author.id:
|
||||
await interaction.response.send_message("This menu is not for you!", ephemeral=True)
|
||||
return
|
||||
|
||||
try:
|
||||
item_id = interaction.data["values"][0].replace("sell_", "")
|
||||
item = self.item_registry[item_id]
|
||||
except (KeyError, AttributeError):
|
||||
await interaction.response.send_message(
|
||||
"❌ This item no longer exists!",
|
||||
ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
# Calculate sell price (25% for perks, 50% for consumables)
|
||||
sell_price = int(item["cost"] * (0.25 if item["type"] == "perk" else 0.5))
|
||||
currency_name = await bank.get_currency_name(self.ctx.guild)
|
||||
|
||||
# Remove item and give credits
|
||||
async with self.cog.config.member(self.ctx.author).all() as member_data:
|
||||
# Clean up expired items first
|
||||
member_data = await cleanup_inventory(member_data, self.item_registry)
|
||||
|
||||
if item["type"] == "perk":
|
||||
if item_id not in member_data.get("purchased_perks", []):
|
||||
await interaction.response.send_message(
|
||||
"❌ You no longer have this perk!",
|
||||
ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
member_data["purchased_perks"].remove(item_id)
|
||||
|
||||
# Special handling for notify_ping
|
||||
if item_id == "notify_ping":
|
||||
member_data["notify_on_release"] = False
|
||||
member_data["notify_unlocked"] = False
|
||||
else:
|
||||
if item_id not in member_data.get("active_items", {}):
|
||||
await interaction.response.send_message(
|
||||
"❌ You no longer have this item!",
|
||||
ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
del member_data["active_items"][item_id]
|
||||
|
||||
await bank.deposit_credits(self.ctx.author, sell_price)
|
||||
self.member_data = member_data
|
||||
|
||||
await interaction.response.send_message(
|
||||
f"💰 Sold {item['emoji']} **{item['name']}** for {sell_price:,} {currency_name}",
|
||||
ephemeral=True
|
||||
)
|
||||
await self._update_message()
|
||||
|
||||
async def interaction_check(self, interaction: discord.Interaction) -> bool:
|
||||
"""Check if the interaction is valid.
|
||||
|
||||
Args:
|
||||
interaction (discord.Interaction): The interaction to check
|
||||
|
||||
Returns:
|
||||
bool: True if the interaction is valid, False otherwise
|
||||
"""
|
||||
return interaction.user.id == self.ctx.author.id
|
||||
|
||||
async def on_timeout(self) -> None:
|
||||
"""Handle view timeout by disabling all components."""
|
||||
try:
|
||||
for child in self.children:
|
||||
child.disabled = True
|
||||
await self.message.edit(view=self)
|
||||
except (discord.NotFound, discord.HTTPException):
|
||||
pass
|
||||
|
||||
async def display_inventory(cog: commands.Cog, ctx: commands.Context, item_registry: Dict[str, Dict[str, Any]]) -> None:
|
||||
"""Display a user's inventory.
|
||||
|
||||
This function creates and displays an interactive view that allows users to
|
||||
manage their inventory items. Users can activate or sell items through the view.
|
||||
|
||||
Args:
|
||||
cog (commands.Cog): The cog instance
|
||||
ctx (commands.Context): The command context
|
||||
item_registry (Dict[str, Dict[str, Any]]): Registry of available items
|
||||
|
||||
Note:
|
||||
The item_registry should contain items in the following format:
|
||||
{
|
||||
"item_id": {
|
||||
"name": str,
|
||||
"description": str,
|
||||
"cost": int,
|
||||
"type": "consumable" | "perk",
|
||||
"emoji": str,
|
||||
"can_sell": bool = True,
|
||||
"duration": int = None, # Optional: duration in seconds
|
||||
"uses": int = None # Optional: number of uses
|
||||
}
|
||||
}
|
||||
"""
|
||||
# Get and clean member data
|
||||
member_data = await cog.config.member(ctx.author).all()
|
||||
member_data = await cleanup_inventory(member_data, item_registry)
|
||||
|
||||
# Save cleaned data
|
||||
await cog.config.member(ctx.author).set(member_data)
|
||||
|
||||
# Create view and initialize
|
||||
view = InventoryView(ctx, cog, member_data, item_registry)
|
||||
|
||||
# Create tasks for parallel execution
|
||||
tasks = [
|
||||
view._create_embed(),
|
||||
view._update_options()
|
||||
]
|
||||
|
||||
# Wait for all tasks to complete
|
||||
embed, _ = await asyncio.gather(*tasks)
|
||||
|
||||
# Send message with embed and view
|
||||
view.message = await ctx.send(embed=embed, view=view)
|
||||
|
||||
def format_time_remaining(seconds: int) -> str:
|
||||
"""Format remaining time into a human-readable string.
|
||||
|
||||
Args:
|
||||
seconds (int): Number of seconds remaining
|
||||
|
||||
Returns:
|
||||
str: Formatted time string (e.g. "2d 5h 30m" or "45m")
|
||||
"""
|
||||
if seconds <= 0:
|
||||
return "0m"
|
||||
|
||||
days = seconds // 86400
|
||||
hours = (seconds % 86400) // 3600
|
||||
minutes = (seconds % 3600) // 60
|
||||
|
||||
parts = []
|
||||
if days > 0:
|
||||
parts.append(f"{days}d")
|
||||
if hours > 0 or (days > 0 and minutes > 0): # Show hours if we have days and minutes
|
||||
parts.append(f"{hours}h")
|
||||
if minutes > 0 or not parts: # Always show minutes if no larger unit
|
||||
parts.append(f"{minutes}m")
|
||||
|
||||
return " ".join(parts)
|
156
city/plan.md
Normal file
156
city/plan.md
Normal file
|
@ -0,0 +1,156 @@
|
|||
# City Cog Development Plan
|
||||
|
||||
## Current Modules
|
||||
|
||||
### 1. Crime Module
|
||||
#### Jail System Refactoring Plan
|
||||
1. **Create Dedicated Jail Module** (`city/crime/jail.py`) ✅
|
||||
- Core JailManager class ✅
|
||||
- Centralized state management ✅
|
||||
- Standardized error handling ✅
|
||||
- Event dispatching system ✅
|
||||
|
||||
2. **JailManager Class Structure**
|
||||
```python
|
||||
class JailManager:
|
||||
# Core Jail Functions ✅
|
||||
- get_jail_time_remaining(member)
|
||||
- send_to_jail(member, time, channel)
|
||||
- release_from_jail(member)
|
||||
- is_in_jail(member)
|
||||
|
||||
# Bail System ✅
|
||||
- calculate_bail_cost(remaining_time)
|
||||
- process_bail_payment(member, amount)
|
||||
- can_pay_bail(member)
|
||||
- format_bail_embed(member)
|
||||
|
||||
# Jailbreak System
|
||||
- get_jailbreak_scenario()
|
||||
- process_jailbreak_attempt(member)
|
||||
- apply_jailbreak_event(member, event)
|
||||
|
||||
# Perk Management ✅
|
||||
- apply_sentence_reduction(time)
|
||||
- has_jail_reducer(member)
|
||||
|
||||
# Notification System ✅
|
||||
- schedule_release_notification(member)
|
||||
- cancel_notification(member)
|
||||
- send_notification(member, channel)
|
||||
|
||||
# State Management ✅
|
||||
- get_jail_state(member)
|
||||
- update_jail_state(member, state)
|
||||
- clear_jail_state(member)
|
||||
```
|
||||
|
||||
3. **Implementation Phases**
|
||||
|
||||
a) Core Infrastructure ✅
|
||||
- [x] Create JailManager class
|
||||
- [x] Implement state management
|
||||
- [x] Set up error handling system
|
||||
- [x] Add logging and debugging
|
||||
|
||||
b) Feature Migration
|
||||
- [x] Move jail time calculations
|
||||
- [x] Migrate bail system
|
||||
- [x] Transfer jailbreak mechanics
|
||||
- [x] Port notification system
|
||||
|
||||
c) Integration Updates
|
||||
- [ ] Update command handlers
|
||||
- [ ] Modify view classes
|
||||
- [ ] Adjust crime scenarios
|
||||
- [ ] Update black market integration
|
||||
|
||||
4. **Data Structure Updates** ✅
|
||||
```python
|
||||
JailState = {
|
||||
"jail_until": int,
|
||||
"attempted_jailbreak": bool,
|
||||
"jail_channel": Optional[int],
|
||||
"notify_on_release": bool,
|
||||
"reduced_sentence": bool,
|
||||
"original_sentence": int
|
||||
}
|
||||
```
|
||||
|
||||
5. **Error Handling** ✅
|
||||
- Custom exception classes
|
||||
- Standardized error messages
|
||||
- Proper error propagation
|
||||
- Recovery mechanisms
|
||||
|
||||
6. **Migration Strategy**
|
||||
|
||||
a) Phase 1: Parallel Implementation
|
||||
- [x] Implement new system alongside existing
|
||||
- [ ] Validate all functionality
|
||||
|
||||
b) Phase 2: Cleanup
|
||||
- [ ] Remove old implementation
|
||||
- [ ] Clean up imports
|
||||
- [ ] Update documentation
|
||||
|
||||
7. **Validation Checklist**
|
||||
- [ ] All existing features work
|
||||
- [ ] UI remains unchanged
|
||||
- [ ] Perks function correctly
|
||||
- [ ] Notifications work reliably
|
||||
- [ ] No performance regression
|
||||
- [ ] State consistency maintained
|
||||
- [ ] Error handling improved
|
||||
|
||||
#### Next Steps (Priority Order)
|
||||
- [ ] Implement jailbreak refactor
|
||||
- [ ] Update command handlers and views
|
||||
- [ ] New Features
|
||||
- [ ] "The Purge" blackmarket item implementation
|
||||
- [ ] Additional balancing and tweaks
|
||||
- [ ] More custom scenarios
|
||||
|
||||
### 2. Business Module (In Development)
|
||||
#### Core Features
|
||||
- [ ] Business creation and management
|
||||
- [ ] Multiple industry types
|
||||
- [ ] Trading
|
||||
- [ ] Manufacturing
|
||||
- [ ] Retail
|
||||
- [ ] Vault system
|
||||
- [ ] Passive income generation
|
||||
- [ ] Business robbery mechanics
|
||||
- [ ] Security systems
|
||||
|
||||
#### Business UI
|
||||
- [ ] Business management interface
|
||||
- [ ] Shop interface
|
||||
- [ ] Status displays
|
||||
- [ ] Transaction history
|
||||
|
||||
#### Business Progression
|
||||
- [ ] Level system
|
||||
- [ ] Upgrades shop
|
||||
- [ ] Industry-specific perks
|
||||
- [ ] Achievement system
|
||||
|
||||
## Planned Modules
|
||||
|
||||
### TBA
|
||||
|
||||
## Technical Improvements
|
||||
|
||||
### Performance
|
||||
- [ ] Optimize database queries
|
||||
- [ ] Cache frequently accessed data
|
||||
- [ ] Memory usage optimization
|
||||
|
||||
### Code Quality
|
||||
- [ ] Documentation improvements
|
||||
- [ ] Code refactoring
|
||||
- [ ] Type hints implementation
|
||||
|
||||
### UI/UX
|
||||
- [ ] Consistent UI
|
||||
- [ ] Consistent error messages
|
293
city/utils.py
Normal file
293
city/utils.py
Normal file
|
@ -0,0 +1,293 @@
|
|||
"""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}%)"
|
78
lootdrop/README.md
Normal file
78
lootdrop/README.md
Normal file
|
@ -0,0 +1,78 @@
|
|||
# LootDrop Cog for Red-DiscordBot
|
||||
|
||||
A fun and engaging cog that creates random loot drops in your Discord channels and threads. Users can claim rewards, build streaks, and participate in party drops with other members!
|
||||
|
||||
## Features
|
||||
|
||||
### Basic Functionality
|
||||
- Random loot drops appear in configured channels and threads
|
||||
- Customizable drop frequency and reward amounts
|
||||
- Support for both text channels and threads
|
||||
- Activity-based drops (only drops in active channels)
|
||||
- Bad outcomes add risk/reward gameplay
|
||||
|
||||
### Streak System
|
||||
- Earn streak bonuses for consecutive successful claims
|
||||
- Configurable streak multipliers and timeouts
|
||||
- Compete for highest streaks on the leaderboard
|
||||
- Streaks reset on bad outcomes or timeout
|
||||
|
||||
### Party Drops
|
||||
- Special drops that multiple users can claim
|
||||
- Scaled rewards based on reaction time:
|
||||
- Super Fast (0-20%): 80-100% of max reward
|
||||
- Fast (20-40%): 60-80% of max reward
|
||||
- Medium (40-60%): 40-60% of max reward
|
||||
- Slow (60-80%): 20-40% of max reward
|
||||
- Very Slow (80-100%): minimum reward
|
||||
- Configurable party drop chance and timeout
|
||||
|
||||
### Statistics & Leaderboard
|
||||
- Track successful and failed claims
|
||||
- View personal stats and streaks
|
||||
- Server-wide leaderboard
|
||||
- Rank tracking for competitive play
|
||||
|
||||
## Commands
|
||||
|
||||
### General Commands
|
||||
- `[p]lootdrop` - Show basic cog info
|
||||
- `[p]lootdrop stats [user]` - View loot drop statistics
|
||||
- `[p]lootdrop leaderboard` - View the server leaderboard
|
||||
- `[p]lootdrop settings` - View current settings
|
||||
|
||||
### Admin Commands
|
||||
- `[p]lootdrop set toggle` - Enable/disable loot drops
|
||||
- `[p]lootdrop set addchannel <channel/thread>` - Add a channel or thread
|
||||
- `[p]lootdrop set removechannel <channel/thread>` - Remove a channel or thread
|
||||
- `[p]lootdrop set credits <min> <max>` - Set credit range
|
||||
- `[p]lootdrop set badchance <chance>` - Set bad outcome chance
|
||||
- `[p]lootdrop set timeout <seconds>` - Set claim timeout
|
||||
- `[p]lootdrop set frequency <min> <max>` - Set drop frequency
|
||||
- `[p]lootdrop set activitytimeout <minutes>` - Set activity timeout
|
||||
- `[p]lootdrop set streakbonus <percentage>` - Set streak bonus
|
||||
- `[p]lootdrop set streakmax <max>` - Set maximum streak
|
||||
- `[p]lootdrop set streaktimeout <hours>` - Set streak timeout
|
||||
- `[p]lootdrop set partychance <chance>` - Set party drop chance
|
||||
- `[p]lootdrop set partycredits <min> <max>` - Set party drop rewards
|
||||
- `[p]lootdrop set partytimeout <seconds>` - Set party claim timeout
|
||||
- `[p]lootdrop force [channel]` - Force a drop
|
||||
- `[p]lootdrop forceparty [channel]` - Force a party drop
|
||||
|
||||
## Installation
|
||||
|
||||
1. Make sure you have Red-DiscordBot V3 installed
|
||||
2. Add this repository: `[p]repo add CalaMari-Cogs https://github.com/CalaMariGold/CalaMari-Cogs`
|
||||
3. Install the cog: `[p]cog install CalaMari-Cogs lootdrop`
|
||||
|
||||
## Setup
|
||||
|
||||
1. Load the cog: `[p]load lootdrop`
|
||||
2. Add channels/threads: `[p]lootdrop set addchannel <channel/thread>`
|
||||
3. Enable drops: `[p]lootdrop set toggle`
|
||||
4. Customize settings as desired using the `[p]lootdrop set` commands
|
||||
|
||||
## Support
|
||||
|
||||
If you encounter any issues or have suggestions, please open an issue on the GitHub repository.
|
||||
|
10
lootdrop/__init__.py
Normal file
10
lootdrop/__init__.py
Normal file
|
@ -0,0 +1,10 @@
|
|||
"""
|
||||
LootDrop cog for Red-DiscordBot by CalaMariGold
|
||||
Random credit drops with good/bad outcomes
|
||||
"""
|
||||
from .lootdrop import LootDrop
|
||||
|
||||
__red_end_user_data_statement__ = "This cog stores user interaction cooldowns and statistics. No personal data is stored."
|
||||
|
||||
async def setup(bot):
|
||||
await bot.add_cog(LootDrop(bot))
|
16
lootdrop/info.json
Normal file
16
lootdrop/info.json
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"name": "LootDrop",
|
||||
"short": "Interactive random resource drops with risk/reward mechanics",
|
||||
"description": "A dynamic resource distribution system that creates random drops in configured channels and threads. Features include streak bonuses, party drops for multiple users, risk/reward mechanics with good/bad outcomes, leaderboards, and detailed statistics tracking.",
|
||||
"end_user_data_statement": "This cog stores user data including:\n- Drop statistics (successful/failed claims)\n- Streak information and history\n- Last claim timestamps for streak tracking\n- Highest achieved streaks\nAll data can be removed using `[p]lootdrop wipestats` or the standard Red delete data commands.",
|
||||
"install_msg": "Thanks for installing LootDrop! Get started with `[p]lootdrop set addchannel` to configure channels, then `[p]lootdrop set toggle` to enable drops.\n\nNotes:\n- Uses Config to store user stats and settings\n- Moderate memory usage for tracking active drops and streaks\n- Requires bank to be registered (`[p]bank register`)\n- Requires manage_messages permission for button interactions\n\nUse `[p]help Lootdrop` for all commands.",
|
||||
"author": ["CalaMariGold"],
|
||||
"required_cogs": {},
|
||||
"requirements": [],
|
||||
"tags": ["economy", "fun", "games"],
|
||||
"min_bot_version": "3.5.0",
|
||||
"min_python_version": [3, 8, 1],
|
||||
"hidden": false,
|
||||
"disabled": false,
|
||||
"type": "COG"
|
||||
}
|
1289
lootdrop/lootdrop.py
Normal file
1289
lootdrop/lootdrop.py
Normal file
File diff suppressed because it is too large
Load diff
404
lootdrop/scenarios.py
Normal file
404
lootdrop/scenarios.py
Normal file
|
@ -0,0 +1,404 @@
|
|||
"""
|
||||
Scenarios for the LootDrop cog
|
||||
Each scenario has:
|
||||
- start: The initial scenario message
|
||||
- good: Good outcome message template
|
||||
- bad: Bad outcome message template
|
||||
- button_text: Text to show on the claim button
|
||||
- button_emoji: Emoji to show on the claim button
|
||||
|
||||
The {user} placeholder will be replaced with the user's mention in outcomes
|
||||
The {amount} placeholder will be replaced with the currency amount
|
||||
The {currency} placeholder will be replaced with the guild's currency name
|
||||
"""
|
||||
from typing import List, Dict, TypedDict
|
||||
|
||||
|
||||
class Scenario(TypedDict):
|
||||
start: str # The initial scenario message
|
||||
good: str # Good outcome message template
|
||||
bad: str # Bad outcome message template
|
||||
button_text: str # Text to show on claim button
|
||||
button_emoji: str # Emoji to show on claim button
|
||||
|
||||
|
||||
SCENARIOS: List[Scenario] = [
|
||||
{
|
||||
"start": "💾 A dusty USB drive labeled 'Top Secret Mod Notes' lies forgotten...",
|
||||
"good": "{user} returns the drive to the mods and gets {amount} {currency} for their honesty!",
|
||||
"bad": "The drive contained a virus! {user} pays {amount} {currency} to the server tech support role.",
|
||||
"button_text": "Check Drive",
|
||||
"button_emoji": "💾"
|
||||
},
|
||||
{
|
||||
"start": "🤖 A wild `LootBot` appears in the channel, sparkling strangely...",
|
||||
"good": "{user} interacts with the bot and it glitches, dropping {amount} {currency}!",
|
||||
"bad": "The bot was a mimic! It bites {user} and steals {amount} {currency}!",
|
||||
"button_text": "Interact",
|
||||
"button_emoji": "🤖"
|
||||
},
|
||||
{
|
||||
"start": "📥 A DM pops up from a new user: 'Click here for free {currency}!!1!'...",
|
||||
"good": "{user} reports the scammer and the server owner rewards them with {amount} {currency}!",
|
||||
"bad": "It was a phishing link! {user} loses {amount} {currency} fixing their account!",
|
||||
"button_text": "Check Link",
|
||||
"button_emoji": "🔗"
|
||||
},
|
||||
{
|
||||
"start": "🌟 A Discord Nitro link appears in the channel...",
|
||||
"good": "{user} claims the Nitro and finds a bonus {amount} {currency}!",
|
||||
"bad": "It was a fake link! {user} feels the sting of disappointment in themselves and was hacked, losing {amount} {currency}.",
|
||||
"button_text": "Claim Nitro",
|
||||
"button_emoji": "✨"
|
||||
},
|
||||
{
|
||||
"start": "🎤 Someone started Karaoke night in the VC, but the next singer is shy...",
|
||||
"good": "{user} belts out a banger! The crowd showers them with {amount} {currency}!",
|
||||
"bad": "{user}'s mic feedback breaks the bot! They pay {amount} {currency} for repairs!",
|
||||
"button_text": "Grab Mic",
|
||||
"button_emoji": "🎤"
|
||||
},
|
||||
{
|
||||
"start": "💰 A suspicious looking wallet lies on the ground...",
|
||||
"good": "{user} steals the wallet full of {amount} {currency} and runs away!",
|
||||
"bad": "{user} was caught by the police! {amount} {currency} fine!",
|
||||
"button_text": "Pick up wallet",
|
||||
"button_emoji": "👛"
|
||||
},
|
||||
{
|
||||
"start": "✨ A mysterious chest materializes out of thin air...",
|
||||
"good": "{user} opens the chest and finds {amount} {currency} worth of treasure!",
|
||||
"bad": "The chest was cursed! {user} pays {amount} {currency} to break free!",
|
||||
"button_text": "Open chest",
|
||||
"button_emoji": "🗝️"
|
||||
},
|
||||
{
|
||||
"start": "🎲 A sketchy merchant appears with a game of chance...",
|
||||
"good": "{user} wins the game and receives {amount} {currency}!",
|
||||
"bad": "{user} loses the game and pays {amount} {currency} to the merchant!",
|
||||
"button_text": "Play game",
|
||||
"button_emoji": "🎲"
|
||||
},
|
||||
{
|
||||
"start": "🎭 A street performer seeks a volunteer from the crowd...",
|
||||
"good": "The crowd loves {user}'s performance! They earn {amount} {currency} in tips!",
|
||||
"bad": "{user} accidentally breaks the props! They pay {amount} {currency} in damages!",
|
||||
"button_text": "Volunteer",
|
||||
"button_emoji": "🎭"
|
||||
},
|
||||
{
|
||||
"start": "📦 An unmarked package sits mysteriously on the doorstep...",
|
||||
"good": "{user} opens a surprise gift containing {amount} {currency}!",
|
||||
"bad": "It's a prank package! {user} spends {amount} {currency} cleaning up the mess!",
|
||||
"button_text": "Open package",
|
||||
"button_emoji": "📦"
|
||||
},
|
||||
{
|
||||
"start": "🎪 An enticing carnival game stand appears...",
|
||||
"good": "{user} wins the jackpot! {amount} {currency} richer!",
|
||||
"bad": "The game was rigged by a rival server! {user} loses {amount} {currency}!",
|
||||
"button_text": "Try your luck",
|
||||
"button_emoji": "🎯"
|
||||
},
|
||||
{
|
||||
"start": "🎮 A vintage arcade cabinet flickers to life...",
|
||||
"good": "{user} finds {amount} {currency} worth of tokens inside!",
|
||||
"bad": "The machine eats {user}'s {currency}! {amount} lost to the void!",
|
||||
"button_text": "Insert coin",
|
||||
"button_emoji": "🕹️"
|
||||
},
|
||||
{
|
||||
"start": "🎣 A golden fishing rod floats in a nearby pond...",
|
||||
"good": "{user} catches a rare fish worth {amount} {currency}!",
|
||||
"bad": "{user} falls in and loses {amount} {currency} worth of electronics!",
|
||||
"button_text": "Cast line",
|
||||
"button_emoji": "🎣"
|
||||
},
|
||||
{
|
||||
"start": "🎨 A street artist offers to paint your portrait...",
|
||||
"good": "The painting sells for {amount} {currency}! {user} gets the profits!",
|
||||
"bad": "The paint was permanent! {user} pays {amount} {currency} for removal!",
|
||||
"button_text": "Pose",
|
||||
"button_emoji": "🎨"
|
||||
},
|
||||
{
|
||||
"start": "🎵 A ghostly music box plays a haunting melody...",
|
||||
"good": "The ghost rewards {user} with {amount} {currency} for listening!",
|
||||
"bad": "The cursed tune costs {user} {amount} {currency} to forget!",
|
||||
"button_text": "Listen closer",
|
||||
"button_emoji": "👻"
|
||||
},
|
||||
{
|
||||
"start": "🌋 A dormant volcano rumbles ominously...",
|
||||
"good": "{user} discovers ancient treasure worth {amount} {currency}!",
|
||||
"bad": "The eruption destroys {user}'s belongings! {amount} {currency} in damages!",
|
||||
"button_text": "Investigate",
|
||||
"button_emoji": "🌋"
|
||||
},
|
||||
{
|
||||
"start": "🎪 A time traveler's briefcase appears in a flash of light...",
|
||||
"good": "{user} finds futuristic currency worth {amount} {currency}!",
|
||||
"bad": "Temporal police fine {user} {amount} {currency} for interference!",
|
||||
"button_text": "Open briefcase",
|
||||
"button_emoji": "⌛"
|
||||
},
|
||||
{
|
||||
"start": "🎰 A malfunctioning vending machine sparks and whirs...",
|
||||
"good": "The machine dispenses {amount} {currency} to {user}!",
|
||||
"bad": "The machine swallows {user}'s card and charges {amount} {currency}!",
|
||||
"button_text": "Press buttons",
|
||||
"button_emoji": "🎰"
|
||||
},
|
||||
{
|
||||
"start": "🎭 An ancient mask whispers secrets of power...",
|
||||
"good": "{user} learns wisdom worth {amount} {currency}!",
|
||||
"bad": "The mask possesses {user}! Exorcism costs {amount} {currency}!",
|
||||
"button_text": "Wear mask",
|
||||
"button_emoji": "👺"
|
||||
},
|
||||
{
|
||||
"start": "🎪 A dimensional rift tears open reality...",
|
||||
"good": "Alternate {user} sends {amount} {currency} through the portal!",
|
||||
"bad": "Evil {user} steals {amount} {currency} and escapes!",
|
||||
"button_text": "Enter portal",
|
||||
"button_emoji": "🌀"
|
||||
},
|
||||
{
|
||||
"start": "🎨 A blank canvas radiates mysterious energy...",
|
||||
"good": "{user}'s artwork magically comes alive, worth {amount} {currency}!",
|
||||
"bad": "The painting traps {user}! Rescue costs {amount} {currency}!",
|
||||
"button_text": "Start painting",
|
||||
"button_emoji": "🖌️"
|
||||
},
|
||||
{
|
||||
"start": "🎭 A magical mirror shows your reflection...",
|
||||
"good": "{user}'s reflection hands over {amount} {currency}!",
|
||||
"bad": "The mirror traps {user}'s shadow! Ransom costs {amount} {currency}!",
|
||||
"button_text": "Touch mirror",
|
||||
"button_emoji": "🎭"
|
||||
},
|
||||
{
|
||||
"start": "🎪 A cosmic vending machine descends from space...",
|
||||
"good": "{user} receives alien technology worth {amount} {currency}!",
|
||||
"bad": "The machine abducts {user}'s {currency}! {amount} lost to space!",
|
||||
"button_text": "Insert {currency}",
|
||||
"button_emoji": "👽"
|
||||
},
|
||||
{
|
||||
"start": "🎲 A mysterious game board draws you in...",
|
||||
"good": "{user} wins the cosmic game and {amount} {currency}!",
|
||||
"bad": "Jumanji-style chaos costs {user} {amount} {currency} to fix!",
|
||||
"button_text": "Roll dice",
|
||||
"button_emoji": "🎲"
|
||||
},
|
||||
{
|
||||
"start": "🌈 A unicorn offers to grant a wish...",
|
||||
"good": "{user}'s pure heart earns {amount} {currency}!",
|
||||
"bad": "The unicorn judges {user} unworthy! Fine of {amount} {currency}!",
|
||||
"button_text": "Make wish",
|
||||
"button_emoji": "🦄"
|
||||
},
|
||||
{
|
||||
"start": "🎭 A sentient shadow offers a deal...",
|
||||
"good": "{user} outsmarts the shadow and gains {amount} {currency}!",
|
||||
"bad": "The shadow steals {user}'s luck! {amount} {currency} to recover!",
|
||||
"button_text": "Make deal",
|
||||
"button_emoji": "🌑"
|
||||
},
|
||||
{
|
||||
"start": "🍦 An ice cream truck is playing its tune late at night...",
|
||||
"good": "{user} gets free unlimited ice cream and {amount} {currency}!",
|
||||
"bad": "It was a trap! {user} pays {amount} {currency} for melted ice cream!",
|
||||
"button_text": "Chase truck",
|
||||
"button_emoji": "🍦"
|
||||
},
|
||||
{
|
||||
"start": "🎰 A casino slot machine is spinning on its own...",
|
||||
"good": "{user} hits the jackpot! {amount} {currency} richer!",
|
||||
"bad": "Security catches {user} and fines them {amount} {currency}!",
|
||||
"button_text": "Pull lever",
|
||||
"button_emoji": "🎰"
|
||||
},
|
||||
{
|
||||
"start": "💎 The jewelry store's alarm system appears to be malfunctioning...",
|
||||
"good": "{user} finds a lost diamond worth {amount} {currency}!",
|
||||
"bad": "The alarm was fake! {user} pays {amount} {currency} in fines!",
|
||||
"button_text": "Investigate",
|
||||
"button_emoji": "💎"
|
||||
},
|
||||
{
|
||||
"start": "🏦 The bank's vault door has opened by itself...",
|
||||
"good": "{user} finds unclaimed {currency} worth {amount}!",
|
||||
"bad": "It was a security test! {user} pays {amount} {currency} in fines!",
|
||||
"button_text": "Peek inside",
|
||||
"button_emoji": "🏦"
|
||||
},
|
||||
{
|
||||
"start": "🚗 A luxury car is parked nearby with keys in the ignition...",
|
||||
"good": "{user} returns the car and gets {amount} {currency} reward!",
|
||||
"bad": "The owner catches {user}! {amount} {currency} fine!",
|
||||
"button_text": "Check car",
|
||||
"button_emoji": "🚗"
|
||||
},
|
||||
{
|
||||
"start": "🎭 A masquerade party is happening at the mansion...",
|
||||
"good": "{user} wins best costume and {amount} {currency}!",
|
||||
"bad": "The mask falls off! {user} pays {amount} {currency} to escape!",
|
||||
"button_text": "Crash party",
|
||||
"button_emoji": "🎭"
|
||||
},
|
||||
{
|
||||
"start": "🖼️ The art gallery's security cameras have stopped working...",
|
||||
"good": "{user} discovers a forgotten masterpiece worth {amount} {currency}!",
|
||||
"bad": "The art was fake! {user} loses {amount} {currency}!",
|
||||
"button_text": "Browse art",
|
||||
"button_emoji": "🖼️"
|
||||
},
|
||||
{
|
||||
"start": "🎪 Music is coming from the circus tent after hours...",
|
||||
"good": "{user} finds {amount} {currency} in lost tickets!",
|
||||
"bad": "The clowns catch {user}! {amount} {currency} to join the show!",
|
||||
"button_text": "Sneak in",
|
||||
"button_emoji": "🎪"
|
||||
},
|
||||
{
|
||||
"start": "🏰 The castle's treasure room door is standing wide open...",
|
||||
"good": "{user} finds ancient coins worth {amount} {currency}!",
|
||||
"bad": "The knights catch {user}! {amount} {currency} fine!",
|
||||
"button_text": "Enter room",
|
||||
"button_emoji": "🏰"
|
||||
},
|
||||
{
|
||||
"start": "🎵 A street musician has left their hat full of coins...",
|
||||
"good": "{user} performs and earns {amount} {currency} in tips!",
|
||||
"bad": "The crowd boos! {user} pays {amount} {currency} to leave!",
|
||||
"button_text": "Join music",
|
||||
"button_emoji": "🎵"
|
||||
},
|
||||
{
|
||||
"start": "🎮 The arcade's machines are running without power...",
|
||||
"good": "{user} rescues {amount} {currency} worth of tokens!",
|
||||
"bad": "The games malfunction! {user} pays {amount} {currency} in damages!",
|
||||
"button_text": "Check games",
|
||||
"button_emoji": "🎮"
|
||||
},
|
||||
{
|
||||
"start": "🚁 A helicopter is sitting idle with keys in the cockpit...",
|
||||
"good": "{user} takes a joyride and finds {amount} {currency}!",
|
||||
"bad": "Crash landing! {user} pays {amount} {currency} in repairs!",
|
||||
"button_text": "Start heli",
|
||||
"button_emoji": "🚁"
|
||||
},
|
||||
{
|
||||
"start": "🎨 The museum is unveiling a mysterious new exhibit...",
|
||||
"good": "{user} discovers a lost artwork worth {amount} {currency}!",
|
||||
"bad": "The curator catches {user}! Fine of {amount} {currency}!",
|
||||
"button_text": "Preview art",
|
||||
"button_emoji": "🎨"
|
||||
},
|
||||
{
|
||||
"start": "🎩 A magician's hat is sitting unattended on stage...",
|
||||
"good": "{user} pulls out {amount} {currency} worth of magic!",
|
||||
"bad": "The rabbit bites! {user} pays {amount} {currency} in damages!",
|
||||
"button_text": "Reach in",
|
||||
"button_emoji": "🎩"
|
||||
},
|
||||
{
|
||||
"start": "🌴 A private beach at the resort is completely empty...",
|
||||
"good": "{user} finds buried treasure worth {amount} {currency}!",
|
||||
"bad": "Security escorts {user} out! {amount} {currency} fine!",
|
||||
"button_text": "Explore beach",
|
||||
"button_emoji": "🌴"
|
||||
},
|
||||
{
|
||||
"start": "🎣 Someone is hosting a fishing event...",
|
||||
"good": "{user} catches the legendary server fish, worth {amount} {currency}!",
|
||||
"bad": "{user} snags their line on the bot's code, costing {amount} {currency} to untangle!",
|
||||
"button_text": "Cast Line",
|
||||
"button_emoji": "🎣"
|
||||
},
|
||||
{
|
||||
"start": "👻 A ghostly ping notification sound echoes, but there's no new message...",
|
||||
"good": "It's a ghost notification! {user} finds the phantom {amount} {currency} left behind!",
|
||||
"bad": "The sound haunts {user}! They pay {amount} {currency} for premium sound packs to forget it!",
|
||||
"button_text": "Investigate Ping",
|
||||
"button_emoji": "👻"
|
||||
},
|
||||
{
|
||||
"start": "📜 A new quest pops up on the server notice board! 'Defeat 10 Spam Bots'...",
|
||||
"good": "{user} completes the quest and earns {amount} {currency} from the Quest Master role!",
|
||||
"bad": "The spam bots overwhelmed {user}! They pay {amount} {currency} for anti-spam protection.",
|
||||
"button_text": "Accept Quest",
|
||||
"button_emoji": "📜"
|
||||
},
|
||||
{
|
||||
"start": "🐲 A wild Server Boss (a glitchy bot?) appears, dropping loot!",
|
||||
"good": "{user} lands the killing blow and gets the Legendary Loot Drop worth {amount} {currency}!",
|
||||
"bad": "The Boss's AOE attack hits {user}! Repair costs are {amount} {currency}!",
|
||||
"button_text": "Attack Boss",
|
||||
"button_emoji": "⚔️"
|
||||
},
|
||||
{
|
||||
"start": "🗺️ An unexplored, dusty channel is discovered...",
|
||||
"good": "{user} finds ancient server lore worth {amount} {currency} to the historians!",
|
||||
"bad": "The channel is haunted by ghost pings! {user} pays {amount} {currency} for mental recovery.",
|
||||
"button_text": "Explore Channel",
|
||||
"button_emoji": "🗺️"
|
||||
},
|
||||
{
|
||||
"start": "🛡️ The server is being DDoSed!",
|
||||
"good": "{user}'s quick thinking helps repel the attack! Rewarded with {amount} {currency} for valor!",
|
||||
"bad": "{user}'s connection drops during the fight! Reconnecting costs {amount} {currency}.",
|
||||
"button_text": "Defend Server",
|
||||
"button_emoji": "🛡️"
|
||||
},
|
||||
{
|
||||
"start": "⚙️ A small, whirring mechanical creature scurries by, trailing sparks...",
|
||||
"good": "{user} catches the creature and finds it carries {amount} {currency}!",
|
||||
"bad": "The creature unleashes an electric shock! {user} pays {amount} {currency} for repairs.",
|
||||
"button_text": "Catch It",
|
||||
"button_emoji": "⚙️"
|
||||
},
|
||||
{
|
||||
"start": "📜 A tattered scroll lies on the path, sealed with an unknown sigil...",
|
||||
"good": "{user} breaks the seal and finds a treasure map leading to {amount} {currency}!",
|
||||
"bad": "The scroll releases a minor curse! {user} pays {amount} {currency} to a local healer.",
|
||||
"button_text": "Read Scroll",
|
||||
"button_emoji": "📜"
|
||||
},
|
||||
{
|
||||
"start": "☄️ A fragment of a falling star lands nearby, glowing softly...",
|
||||
"good": "{user} carefully picks up the star fragment, finding it's worth {amount} {currency}!",
|
||||
"bad": "The fragment burns {user}'s hand! Ointment costs {amount} {currency}.",
|
||||
"button_text": "Touch Fragment",
|
||||
"button_emoji": "☄️"
|
||||
},
|
||||
{
|
||||
"start": "👻 A faint, chilling whisper seems to echo from the shadows...",
|
||||
"good": "{user} follows the whisper and finds {amount} {currency} hidden by a restless spirit!",
|
||||
"bad": "The whisper drains {user}'s energy! A potion costs {amount} {currency}.",
|
||||
"button_text": "Follow Whisper",
|
||||
"button_emoji": "👻"
|
||||
},
|
||||
{
|
||||
"start": "📜 A bounty is posted... 'Clear out the mischievous imps plaguing the area'",
|
||||
"good": "{user} bravely defeats the imps and collects the {amount} {currency} reward!",
|
||||
"bad": "The imps played tricks on {user}, stealing {amount} {currency}!",
|
||||
"button_text": "Accept Bounty",
|
||||
"button_emoji": "📜"
|
||||
},
|
||||
{
|
||||
"start": "🗿 A hulking golem, crafted from stone and metal, blocks the path!",
|
||||
"good": "{user} finds the golem's weak spot and disables it, finding {amount} {currency} inside!",
|
||||
"bad": "The golem smashes {user}'s backpack! Replacing gear costs {amount} {currency}.",
|
||||
"button_text": "Fight Golem",
|
||||
"button_emoji": "🗿"
|
||||
},
|
||||
{
|
||||
"start": "⛈️ A sudden, unnatural storm gathers overhead...",
|
||||
"good": "{user} finds shelter and discovers {amount} {currency} left by another traveler!",
|
||||
"bad": "A lightning strike nearby scares {user}, causing them to drop {amount} {currency}!",
|
||||
"button_text": "Seek Shelter",
|
||||
"button_emoji": "⛈️"
|
||||
}
|
||||
]
|
Loading…
Add table
Reference in a new issue