EveCogs/nexuscore/economy.py
everestmcarthur a76758f61a feat: NexusCore v1.0.0 — Ultimate all-in-one server management cog
8 fully-featured subsystems, 5,673 lines of code, 100+ commands:

🎫 Tickets — Panel-based creation, categories with modal questions, staff claim,
   priority system (low/med/high/urgent), HTML transcripts, feedback ratings, DM notifs

📋 Applications — Multi-type forms with paginated modals, accept/deny/interview
   review, auto-role, staff notes, auto-thread, cooldowns, age requirements

💡 Suggestions — Button voting, 8 status types, anonymous mode, auto-thread,
   staff responses, categories, top leaderboard, status-specific channels

🎭 Reaction Roles — Button/select/reaction modes, exclusive groups, role
   requirements, sticky roles, temporary roles, max picks, DM confirm

🎉 Giveaways — Button entry, role/age requirements, bonus entries, drop mode,
   multiple winners, reroll, scheduled end, DM notifications

📋 Logging — 20+ event types, per-event channels, message cache, invite tracking,
   ignore filters (channel/role/user/bot), bulk delete file export

🛡️ Moderation — Case system, warn auto-escalation, mute/kick/ban/softban/tempban,
   notes, channel/server lockdown, anti-raid, anti-nuke, automod (6 modules), appeals

🪙 Economy — Wallet+bank, daily/weekly streaks, work/crime/rob, coinflip/slots/blackjack,
   shop (items+roles), inventory, heist mini-game, pet system (6 types), leaderboard
2026-05-07 23:51:53 +00:00

828 lines
38 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""NexusCore — Economy: currency, bank, daily, work, crime, shop, gambling, leaderboards, pets, heists."""
from __future__ import annotations
import asyncio
import datetime
import random
from typing import Optional
import discord
from redbot.core import Config, commands
from .utils import (
Clr, ok_embed, err_embed, info_embed, short_id, ts_now, ts_relative,
duration_str, safe_send, safe_dm, Paginator, chunk_list,
)
# ── Defaults ───────────────────────────────────────────────────────────────
ECO_DEFAULTS_GUILD = {
"enabled": True,
"currency_name": "coins",
"currency_emoji": "🪙",
"currency_symbol": "$",
"starting_balance": 100,
"max_balance": 10000000,
"log_channel": None,
"daily_amount": 200,
"daily_streak_bonus": 50,
"weekly_amount": 1500,
"work_min": 50,
"work_max": 300,
"work_cooldown": 3600,
"work_messages": [
"You worked as a programmer and earned {amount}!",
"You delivered packages and earned {amount}!",
"You fixed a car and earned {amount}!",
"You tutored students and earned {amount}!",
"You walked dogs and earned {amount}!",
"You painted a house and earned {amount}!",
"You drove a taxi and earned {amount}!",
"You cooked meals and earned {amount}!",
],
"crime_min": 100,
"crime_max": 800,
"crime_cooldown": 7200,
"crime_fail_chance": 40,
"crime_fine_min": 50,
"crime_fine_max": 400,
"crime_messages": [
"You robbed a bank and got {amount}!",
"You hacked into a mainframe and stole {amount}!",
"You ran a scam and pocketed {amount}!",
"You sold counterfeit goods for {amount}!",
],
"crime_fail_messages": [
"You got caught and fined {fine}!",
"The police caught you! You lost {fine}.",
"Your heist failed! You paid {fine} in fines.",
],
"rob_enabled": True,
"rob_cooldown": 14400,
"rob_success_chance": 45,
"rob_min_percent": 5,
"rob_max_percent": 30,
"rob_min_target_balance": 500,
"interest_rate": 0.5,
"interest_interval": 86400,
"tax_rate": 0,
"shop_items": {},
# item_id -> {name, description, price, emoji, role_id, stock, max_per_user, usable, on_use_msg, type: "role"|"item"|"consumable"}
"gambling": {
"coinflip_enabled": True,
"slots_enabled": True,
"blackjack_enabled": True,
"roulette_enabled": True,
"dice_enabled": True,
"max_bet": 50000,
"min_bet": 10,
"slots_jackpot_multi": 10,
"slots_three_multi": 5,
"slots_two_multi": 2,
"blackjack_multi": 2,
},
"pets": {
"enabled": True,
"types": {
"cat": {"emoji": "🐱", "base_price": 500, "daily_earn": 20},
"dog": {"emoji": "🐶", "base_price": 500, "daily_earn": 20},
"bird": {"emoji": "🐦", "base_price": 300, "daily_earn": 15},
"fish": {"emoji": "🐟", "base_price": 200, "daily_earn": 10},
"dragon": {"emoji": "🐉", "base_price": 5000, "daily_earn": 100},
"unicorn": {"emoji": "🦄", "base_price": 10000, "daily_earn": 200},
},
},
"heist": {
"enabled": True,
"min_players": 2,
"max_players": 10,
"join_time": 60,
"min_bet": 100,
"base_success": 50,
"per_player_bonus": 5,
"payout_multi": 3,
"cooldown": 3600,
},
}
ECO_DEFAULTS_MEMBER = {
"wallet": 0,
"bank": 0,
"bank_limit": 10000,
"total_earned": 0,
"total_spent": 0,
"daily_streak": 0,
"last_daily": 0,
"last_weekly": 0,
"last_work": 0,
"last_crime": 0,
"last_rob": 0,
"last_heist": 0,
"inventory": {}, # item_id -> count
"pets": {}, # pet_name -> {type, level, xp, fed_at, happiness}
"transactions": [], # last 50 transactions
}
# ── Slot emojis ────────────────────────────────────────────────────────────
SLOT_EMOJIS = ["🍒", "🍋", "🍊", "🍇", "🍉", "⭐", "💎", "7️⃣"]
# ── Views ──────────────────────────────────────────────────────────────────
class HeistJoinView(discord.ui.View):
def __init__(self, cog, leader: discord.Member, bet: int):
super().__init__(timeout=70)
self.cog = cog
self.leader = leader
self.bet = bet
self.participants = {leader.id}
@discord.ui.button(label="🔫 Join Heist", style=discord.ButtonStyle.danger)
async def join_btn(self, interaction: discord.Interaction, button: discord.ui.Button):
if interaction.user.id in self.participants:
return await interaction.response.send_message("Already in!", ephemeral=True)
bal = await self.cog.eco_config.member(interaction.user).wallet()
if bal < self.bet:
return await interaction.response.send_message(f"You need {self.bet} in your wallet.", ephemeral=True)
heist_data = await self.cog.eco_config.guild(interaction.guild).heist()
if len(self.participants) >= heist_data.get("max_players", 10):
return await interaction.response.send_message("Heist is full!", ephemeral=True)
self.participants.add(interaction.user.id)
await interaction.response.send_message(
f"🔫 {interaction.user.display_name} joined! ({len(self.participants)} members)",
)
@discord.ui.button(label="Participants: 1", style=discord.ButtonStyle.secondary, disabled=True)
async def count_btn(self, interaction: discord.Interaction, button: discord.ui.Button):
pass
class BlackjackView(discord.ui.View):
def __init__(self, cog, player: discord.Member, bet: int, player_hand: list, dealer_hand: list, deck: list):
super().__init__(timeout=60)
self.cog = cog
self.player = player
self.bet = bet
self.player_hand = player_hand
self.dealer_hand = dealer_hand
self.deck = deck
self.stood = False
def _hand_value(self, hand):
value = 0
aces = 0
for card in hand:
if card[0] in ("J", "Q", "K"):
value += 10
elif card[0] == "A":
aces += 1
value += 11
else:
value += int(card[0])
while value > 21 and aces:
value -= 10
aces -= 1
return value
def _hand_str(self, hand, hide_second=False):
suits = {"H": "♥️", "D": "♦️", "C": "♣️", "S": "♠️"}
if hide_second and len(hand) > 1:
return f"`{hand[0][0]}{suits.get(hand[0][1], '?')}` `??`"
return " ".join(f"`{c[0]}{suits.get(c[1], '?')}`" for c in hand)
def _build_embed(self, reveal=False):
pv = self._hand_value(self.player_hand)
embed = discord.Embed(title="🃏 Blackjack", colour=Clr.ECO)
embed.add_field(
name=f"Dealer {f'({self._hand_value(self.dealer_hand)})' if reveal else ''}",
value=self._hand_str(self.dealer_hand, hide_second=not reveal),
inline=False,
)
embed.add_field(
name=f"You ({pv})",
value=self._hand_str(self.player_hand),
inline=False,
)
embed.set_footer(text=f"Bet: {self.bet}")
return embed
async def interaction_check(self, interaction: discord.Interaction) -> bool:
if interaction.user.id != self.player.id:
await interaction.response.send_message("Not your game.", ephemeral=True)
return False
return True
@discord.ui.button(label="Hit", style=discord.ButtonStyle.primary, emoji="🃏")
async def hit_btn(self, interaction: discord.Interaction, button: discord.ui.Button):
self.player_hand.append(self.deck.pop())
pv = self._hand_value(self.player_hand)
if pv > 21:
embed = self._build_embed(reveal=True)
embed.description = f"💥 Bust! You lost **{self.bet}**."
embed.colour = Clr.ERROR
await self.cog._add_transaction(interaction.user, -self.bet, "Blackjack loss")
self.stop()
await interaction.response.edit_message(embed=embed, view=None)
elif pv == 21:
await self._resolve(interaction)
else:
await interaction.response.edit_message(embed=self._build_embed())
@discord.ui.button(label="Stand", style=discord.ButtonStyle.secondary, emoji="✋")
async def stand_btn(self, interaction: discord.Interaction, button: discord.ui.Button):
await self._resolve(interaction)
@discord.ui.button(label="Double Down", style=discord.ButtonStyle.danger, emoji="💰")
async def double_btn(self, interaction: discord.Interaction, button: discord.ui.Button):
bal = await self.cog.eco_config.member(interaction.user).wallet()
if bal < self.bet:
return await interaction.response.send_message("Not enough to double.", ephemeral=True)
self.bet *= 2
self.player_hand.append(self.deck.pop())
await self._resolve(interaction)
async def _resolve(self, interaction: discord.Interaction):
self.stop()
# Dealer draws
while self._hand_value(self.dealer_hand) < 17:
self.dealer_hand.append(self.deck.pop())
pv = self._hand_value(self.player_hand)
dv = self._hand_value(self.dealer_hand)
embed = self._build_embed(reveal=True)
if pv > 21:
result = "lose"
elif dv > 21:
result = "win"
elif pv > dv:
result = "win"
elif pv < dv:
result = "lose"
else:
result = "tie"
settings = await self.cog.eco_config.guild(interaction.guild).gambling()
multi = settings.get("blackjack_multi", 2)
if result == "win":
winnings = int(self.bet * multi)
embed.description = f"🎉 You won **{winnings}**!"
embed.colour = Clr.SUCCESS
await self.cog._add_balance(interaction.user, winnings)
await self.cog._add_transaction(interaction.user, winnings, "Blackjack win")
elif result == "lose":
embed.description = f"💔 You lost **{self.bet}**."
embed.colour = Clr.ERROR
await self.cog._add_transaction(interaction.user, -self.bet, "Blackjack loss")
else:
embed.description = f"🤝 Push! Bet returned."
embed.colour = Clr.INFO
await self.cog._add_balance(interaction.user, self.bet)
await interaction.response.edit_message(embed=embed, view=None)
class ShopView(discord.ui.View):
def __init__(self, cog, guild: discord.Guild, items: dict):
super().__init__(timeout=120)
self.cog = cog
options = []
for item_id, item in list(items.items())[:25]:
if item.get("stock", -1) == 0:
continue
options.append(discord.SelectOption(
label=item["name"],
value=item_id,
description=f"{item['price']} {item.get('emoji', '🪙')} — {(item.get('description', '') or '')[:80]}",
emoji=item.get("emoji"),
))
if options:
self.sel = discord.ui.Select(placeholder="Buy an item...", options=options)
self.sel.callback = self.on_select
self.add_item(self.sel)
self.guild = guild
async def on_select(self, interaction: discord.Interaction):
item_id = self.sel.values[0]
await self.cog._buy_item(interaction, item_id)
# ── Mixin ──────────────────────────────────────────────────────────────────
class EconomyMixin:
"""Economy system mixin."""
def _init_economy(self, bot):
self.eco_config = Config.get_conf(
None, identifier=900008, cog_name="NexusCoreEco"
)
self.eco_config.register_guild(**ECO_DEFAULTS_GUILD)
self.eco_config.register_member(**ECO_DEFAULTS_MEMBER)
self.bot = bot
# ── Balance helpers ────────────────────────────────────────────────────
async def _get_balance(self, member: discord.Member) -> tuple[int, int]:
data = await self.eco_config.member(member).all()
return data["wallet"], data["bank"]
async def _add_balance(self, member: discord.Member, amount: int, to: str = "wallet"):
if to == "wallet":
current = await self.eco_config.member(member).wallet()
await self.eco_config.member(member).wallet.set(current + amount)
else:
current = await self.eco_config.member(member).bank()
await self.eco_config.member(member).bank.set(current + amount)
if amount > 0:
earned = await self.eco_config.member(member).total_earned()
await self.eco_config.member(member).total_earned.set(earned + amount)
async def _remove_balance(self, member: discord.Member, amount: int, from_: str = "wallet") -> bool:
if from_ == "wallet":
current = await self.eco_config.member(member).wallet()
else:
current = await self.eco_config.member(member).bank()
if current < amount:
return False
if from_ == "wallet":
await self.eco_config.member(member).wallet.set(current - amount)
else:
await self.eco_config.member(member).bank.set(current - amount)
spent = await self.eco_config.member(member).total_spent()
await self.eco_config.member(member).total_spent.set(spent + amount)
return True
async def _add_transaction(self, member: discord.Member, amount: int, description: str):
async with self.eco_config.member(member).transactions() as txns:
txns.append({"amount": amount, "desc": description, "at": ts_now()})
if len(txns) > 50:
txns.pop(0)
async def _format_amount(self, guild: discord.Guild, amount: int) -> str:
data = await self.eco_config.guild(guild).all()
return f"{data['currency_emoji']} {amount:,} {data['currency_name']}"
# ── Daily ──────────────────────────────────────────────────────────────
async def _daily(self, ctx: commands.Context):
member = ctx.author
guild_data = await self.eco_config.guild(ctx.guild).all()
member_data = await self.eco_config.member(member).all()
now = ts_now()
last = member_data["last_daily"]
if now - last < 86400:
remaining = 86400 - (now - last)
return await ctx.send(embed=err_embed(f"Daily available {ts_relative(now + remaining)}"))
streak = member_data["daily_streak"]
# Check if within 48h for streak
if last and now - last < 172800:
streak += 1
else:
streak = 1
amount = guild_data["daily_amount"] + (guild_data["daily_streak_bonus"] * (streak - 1))
await self._add_balance(member, amount)
await self.eco_config.member(member).last_daily.set(now)
await self.eco_config.member(member).daily_streak.set(streak)
await self._add_transaction(member, amount, "Daily reward")
embed = discord.Embed(
title="📅 Daily Reward",
description=f"You received **{await self._format_amount(ctx.guild, amount)}**!",
colour=Clr.ECO,
)
if streak > 1:
embed.add_field(name="🔥 Streak", value=f"{streak} days (+{guild_data['daily_streak_bonus'] * (streak - 1)} bonus)")
await ctx.send(embed=embed)
# ── Weekly ─────────────────────────────────────────────────────────────
async def _weekly(self, ctx: commands.Context):
member = ctx.author
guild_data = await self.eco_config.guild(ctx.guild).all()
last = await self.eco_config.member(member).last_weekly()
now = ts_now()
if now - last < 604800:
remaining = 604800 - (now - last)
return await ctx.send(embed=err_embed(f"Weekly available {ts_relative(now + remaining)}"))
amount = guild_data["weekly_amount"]
await self._add_balance(member, amount)
await self.eco_config.member(member).last_weekly.set(now)
await self._add_transaction(member, amount, "Weekly reward")
await ctx.send(embed=ok_embed(f"You received **{await self._format_amount(ctx.guild, amount)}**!"))
# ── Work ───────────────────────────────────────────────────────────────
async def _work(self, ctx: commands.Context):
guild_data = await self.eco_config.guild(ctx.guild).all()
last = await self.eco_config.member(ctx.author).last_work()
now = ts_now()
cd = guild_data["work_cooldown"]
if now - last < cd:
return await ctx.send(embed=err_embed(f"Work available {ts_relative(now + cd - (now - last))}"))
amount = random.randint(guild_data["work_min"], guild_data["work_max"])
msg = random.choice(guild_data["work_messages"]).format(
amount=await self._format_amount(ctx.guild, amount)
)
await self._add_balance(ctx.author, amount)
await self.eco_config.member(ctx.author).last_work.set(now)
await self._add_transaction(ctx.author, amount, "Work")
await ctx.send(embed=discord.Embed(description=f"💼 {msg}", colour=Clr.ECO))
# ── Crime ──────────────────────────────────────────────────────────────
async def _crime(self, ctx: commands.Context):
guild_data = await self.eco_config.guild(ctx.guild).all()
last = await self.eco_config.member(ctx.author).last_crime()
now = ts_now()
cd = guild_data["crime_cooldown"]
if now - last < cd:
return await ctx.send(embed=err_embed(f"Crime available {ts_relative(now + cd - (now - last))}"))
await self.eco_config.member(ctx.author).last_crime.set(now)
if random.randint(1, 100) <= guild_data["crime_fail_chance"]:
fine = random.randint(guild_data["crime_fine_min"], guild_data["crime_fine_max"])
await self._remove_balance(ctx.author, min(fine, (await self.eco_config.member(ctx.author).wallet())))
msg = random.choice(guild_data["crime_fail_messages"]).format(
fine=await self._format_amount(ctx.guild, fine)
)
await self._add_transaction(ctx.author, -fine, "Crime (failed)")
await ctx.send(embed=discord.Embed(description=f"🚔 {msg}", colour=Clr.ERROR))
else:
amount = random.randint(guild_data["crime_min"], guild_data["crime_max"])
msg = random.choice(guild_data["crime_messages"]).format(
amount=await self._format_amount(ctx.guild, amount)
)
await self._add_balance(ctx.author, amount)
await self._add_transaction(ctx.author, amount, "Crime")
await ctx.send(embed=discord.Embed(description=f"🦹 {msg}", colour=Clr.ECO))
# ── Rob ────────────────────────────────────────────────────────────────
async def _rob(self, ctx: commands.Context, target: discord.Member):
guild_data = await self.eco_config.guild(ctx.guild).all()
if not guild_data["rob_enabled"]:
return await ctx.send(embed=err_embed("Robbing is disabled."))
if target.id == ctx.author.id:
return await ctx.send(embed=err_embed("You can't rob yourself."))
if target.bot:
return await ctx.send(embed=err_embed("You can't rob bots."))
last = await self.eco_config.member(ctx.author).last_rob()
now = ts_now()
cd = guild_data["rob_cooldown"]
if now - last < cd:
return await ctx.send(embed=err_embed(f"Rob available {ts_relative(now + cd - (now - last))}"))
target_wallet = await self.eco_config.member(target).wallet()
if target_wallet < guild_data["rob_min_target_balance"]:
return await ctx.send(embed=err_embed(f"Target needs at least {guild_data['rob_min_target_balance']} in wallet."))
await self.eco_config.member(ctx.author).last_rob.set(now)
if random.randint(1, 100) <= guild_data["rob_success_chance"]:
percent = random.randint(guild_data["rob_min_percent"], guild_data["rob_max_percent"])
stolen = int(target_wallet * percent / 100)
stolen = max(1, stolen)
await self._remove_balance(target, stolen)
await self._add_balance(ctx.author, stolen)
await self._add_transaction(ctx.author, stolen, f"Robbed {target}")
await self._add_transaction(target, -stolen, f"Robbed by {ctx.author}")
await ctx.send(embed=discord.Embed(
description=f"🔫 You robbed **{await self._format_amount(ctx.guild, stolen)}** from {target.mention}!",
colour=Clr.ECO,
))
else:
fine = random.randint(50, 200)
wallet = await self.eco_config.member(ctx.author).wallet()
fine = min(fine, wallet)
await self._remove_balance(ctx.author, fine)
await self._add_transaction(ctx.author, -fine, f"Failed rob on {target}")
await ctx.send(embed=discord.Embed(
description=f"🚔 You failed to rob {target.mention} and lost **{await self._format_amount(ctx.guild, fine)}**!",
colour=Clr.ERROR,
))
# ── Deposit / Withdraw ─────────────────────────────────────────────────
async def _deposit(self, ctx: commands.Context, amount: int):
wallet = await self.eco_config.member(ctx.author).wallet()
bank = await self.eco_config.member(ctx.author).bank()
bank_limit = await self.eco_config.member(ctx.author).bank_limit()
if amount > wallet:
return await ctx.send(embed=err_embed("Not enough in wallet."))
if bank + amount > bank_limit:
return await ctx.send(embed=err_embed(f"Bank limit is {bank_limit:,}. You can deposit {bank_limit - bank:,}."))
await self._remove_balance(ctx.author, amount, "wallet")
await self._add_balance(ctx.author, amount, "bank")
await ctx.send(embed=ok_embed(f"Deposited **{await self._format_amount(ctx.guild, amount)}** to bank."))
async def _withdraw(self, ctx: commands.Context, amount: int):
bank = await self.eco_config.member(ctx.author).bank()
if amount > bank:
return await ctx.send(embed=err_embed("Not enough in bank."))
await self._remove_balance(ctx.author, amount, "bank")
await self._add_balance(ctx.author, amount, "wallet")
await ctx.send(embed=ok_embed(f"Withdrew **{await self._format_amount(ctx.guild, amount)}** to wallet."))
# ── Gambling: Coinflip ─────────────────────────────────────────────────
async def _coinflip(self, ctx: commands.Context, bet: int, choice: str):
settings = await self.eco_config.guild(ctx.guild).gambling()
if not settings.get("coinflip_enabled"):
return await ctx.send(embed=err_embed("Coinflip is disabled."))
if bet < settings["min_bet"] or bet > settings["max_bet"]:
return await ctx.send(embed=err_embed(f"Bet must be {settings['min_bet']:,}–{settings['max_bet']:,}."))
wallet = await self.eco_config.member(ctx.author).wallet()
if bet > wallet:
return await ctx.send(embed=err_embed("Not enough coins."))
await self._remove_balance(ctx.author, bet)
result = random.choice(["heads", "tails"])
won = choice.lower() in (result, result[0])
if won:
winnings = bet * 2
await self._add_balance(ctx.author, winnings)
await self._add_transaction(ctx.author, bet, "Coinflip win")
embed = discord.Embed(
title=f"🪙 {result.title()}!",
description=f"You won **{await self._format_amount(ctx.guild, winnings)}**!",
colour=Clr.SUCCESS,
)
else:
await self._add_transaction(ctx.author, -bet, "Coinflip loss")
embed = discord.Embed(
title=f"🪙 {result.title()}!",
description=f"You lost **{await self._format_amount(ctx.guild, bet)}**.",
colour=Clr.ERROR,
)
await ctx.send(embed=embed)
# ── Gambling: Slots ────────────────────────────────────────────────────
async def _slots(self, ctx: commands.Context, bet: int):
settings = await self.eco_config.guild(ctx.guild).gambling()
if not settings.get("slots_enabled"):
return await ctx.send(embed=err_embed("Slots are disabled."))
if bet < settings["min_bet"] or bet > settings["max_bet"]:
return await ctx.send(embed=err_embed(f"Bet must be {settings['min_bet']:,}–{settings['max_bet']:,}."))
wallet = await self.eco_config.member(ctx.author).wallet()
if bet > wallet:
return await ctx.send(embed=err_embed("Not enough coins."))
await self._remove_balance(ctx.author, bet)
reels = [random.choice(SLOT_EMOJIS) for _ in range(3)]
if reels[0] == reels[1] == reels[2]:
if reels[0] == "💎":
multi = settings["slots_jackpot_multi"]
title = "💎 JACKPOT! 💎"
else:
multi = settings["slots_three_multi"]
title = "🎰 Three of a kind!"
winnings = bet * multi
colour = Clr.SUCCESS
elif reels[0] == reels[1] or reels[1] == reels[2] or reels[0] == reels[2]:
multi = settings["slots_two_multi"]
winnings = bet * multi
title = "🎰 Two matching!"
colour = Clr.ECO
else:
winnings = 0
title = "🎰 No match"
colour = Clr.ERROR
if winnings:
await self._add_balance(ctx.author, winnings)
await self._add_transaction(ctx.author, winnings - bet, "Slots")
desc = f"**[ {' | '.join(reels)} ]**\n\nYou won **{await self._format_amount(ctx.guild, winnings)}**!"
else:
await self._add_transaction(ctx.author, -bet, "Slots loss")
desc = f"**[ {' | '.join(reels)} ]**\n\nYou lost **{await self._format_amount(ctx.guild, bet)}**."
await ctx.send(embed=discord.Embed(title=title, description=desc, colour=colour))
# ── Gambling: Blackjack ────────────────────────────────────────────────
async def _blackjack(self, ctx: commands.Context, bet: int):
settings = await self.eco_config.guild(ctx.guild).gambling()
if not settings.get("blackjack_enabled"):
return await ctx.send(embed=err_embed("Blackjack is disabled."))
if bet < settings["min_bet"] or bet > settings["max_bet"]:
return await ctx.send(embed=err_embed(f"Bet must be {settings['min_bet']:,}–{settings['max_bet']:,}."))
wallet = await self.eco_config.member(ctx.author).wallet()
if bet > wallet:
return await ctx.send(embed=err_embed("Not enough coins."))
await self._remove_balance(ctx.author, bet)
ranks = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"]
suits = ["H", "D", "C", "S"]
deck = [(r, s) for r in ranks for s in suits]
random.shuffle(deck)
player_hand = [deck.pop(), deck.pop()]
dealer_hand = [deck.pop(), deck.pop()]
view = BlackjackView(self, ctx.author, bet, player_hand, dealer_hand, deck)
embed = view._build_embed()
await ctx.send(embed=embed, view=view)
# ── Shop ───────────────────────────────────────────────────────────────
async def _buy_item(self, interaction: discord.Interaction, item_id: str):
guild_data = await self.eco_config.guild(interaction.guild).all()
item = guild_data["shop_items"].get(item_id)
if not item:
return await interaction.response.send_message("Item not found.", ephemeral=True)
wallet = await self.eco_config.member(interaction.user).wallet()
if wallet < item["price"]:
return await interaction.response.send_message("Not enough coins!", ephemeral=True)
member_data = await self.eco_config.member(interaction.user).all()
inv = member_data.get("inventory", {})
current_count = inv.get(item_id, 0)
if item.get("max_per_user") and current_count >= item["max_per_user"]:
return await interaction.response.send_message("You have the max amount of this item.", ephemeral=True)
if item.get("stock", -1) == 0:
return await interaction.response.send_message("Out of stock!", ephemeral=True)
await self._remove_balance(interaction.user, item["price"])
async with self.eco_config.member(interaction.user).inventory() as inv:
inv[item_id] = inv.get(item_id, 0) + 1
if item.get("stock", -1) > 0:
async with self.eco_config.guild(interaction.guild).shop_items() as items:
if item_id in items:
items[item_id]["stock"] -= 1
if item.get("type") == "role" and item.get("role_id"):
role = interaction.guild.get_role(item["role_id"])
if role:
try:
await interaction.user.add_roles(role, reason="NexusCore shop purchase")
except discord.HTTPException:
pass
await self._add_transaction(interaction.user, -item["price"], f"Bought {item['name']}")
emoji = item.get("emoji", "🛒")
await interaction.response.send_message(
f"{emoji} Purchased **{item['name']}** for **{item['price']:,}**!", ephemeral=True
)
# ── Heist ──────────────────────────────────────────────────────────────
async def _start_heist(self, ctx: commands.Context, bet: int):
guild_data = await self.eco_config.guild(ctx.guild).all()
heist = guild_data.get("heist", {})
if not heist.get("enabled"):
return await ctx.send(embed=err_embed("Heists are disabled."))
last = await self.eco_config.member(ctx.author).last_heist()
now = ts_now()
cd = heist.get("cooldown", 3600)
if now - last < cd:
return await ctx.send(embed=err_embed(f"Heist available {ts_relative(now + cd - (now - last))}"))
if bet < heist.get("min_bet", 100):
return await ctx.send(embed=err_embed(f"Minimum bet: {heist['min_bet']:,}"))
wallet = await self.eco_config.member(ctx.author).wallet()
if wallet < bet:
return await ctx.send(embed=err_embed("Not enough coins."))
view = HeistJoinView(self, ctx.author, bet)
embed = discord.Embed(
title="🔫 HEIST — Recruiting!",
description=f"**{ctx.author.display_name}** is planning a heist!\n\nBet: **{bet:,}** each\nClick below to join!\n\nStarting {ts_relative(now + heist.get('join_time', 60))}",
colour=Clr.ECO,
)
msg = await ctx.send(embed=embed, view=view)
await asyncio.sleep(heist.get("join_time", 60))
participants = list(view.participants)
if len(participants) < heist.get("min_players", 2):
embed.description = "❌ Not enough players. Heist cancelled."
embed.colour = Clr.ERROR
return await msg.edit(embed=embed, view=None)
# Deduct bets
for uid in participants:
member = ctx.guild.get_member(uid)
if member:
await self._remove_balance(member, bet)
# Calculate success
base = heist.get("base_success", 50)
bonus = heist.get("per_player_bonus", 5)
success_chance = min(90, base + bonus * len(participants))
success = random.randint(1, 100) <= success_chance
if success:
payout = int(bet * heist.get("payout_multi", 3))
for uid in participants:
member = ctx.guild.get_member(uid)
if member:
await self._add_balance(member, payout)
await self._add_transaction(member, payout - bet, "Heist success")
await self.eco_config.member(member).last_heist.set(now)
mentions = ", ".join(f"<@{uid}>" for uid in participants)
embed = discord.Embed(
title="🔫 HEIST SUCCESS!",
description=f"The crew pulled it off!\n\n{mentions}\n\nEach member earned **{payout:,}**!",
colour=Clr.SUCCESS,
)
else:
for uid in participants:
member = ctx.guild.get_member(uid)
if member:
await self._add_transaction(member, -bet, "Heist failed")
await self.eco_config.member(member).last_heist.set(now)
embed = discord.Embed(
title="🚔 HEIST FAILED!",
description=f"The heist went wrong! Everyone lost their **{bet:,}** bet.",
colour=Clr.ERROR,
)
await msg.edit(embed=embed, view=None)
# ── Pets ───────────────────────────────────────────────────────────────
async def _buy_pet(self, ctx: commands.Context, pet_type: str, name: str):
guild_data = await self.eco_config.guild(ctx.guild).all()
pets_conf = guild_data.get("pets", {})
if not pets_conf.get("enabled"):
return await ctx.send(embed=err_embed("Pets are disabled."))
types = pets_conf.get("types", {})
if pet_type not in types:
available = ", ".join(f"{v['emoji']} {k}" for k, v in types.items())
return await ctx.send(embed=err_embed(f"Unknown type. Available: {available}"))
pet_info = types[pet_type]
price = pet_info["base_price"]
wallet = await self.eco_config.member(ctx.author).wallet()
if wallet < price:
return await ctx.send(embed=err_embed(f"You need {price:,} coins."))
async with self.eco_config.member(ctx.author).pets() as pets:
if name in pets:
return await ctx.send(embed=err_embed(f"You already have a pet named `{name}`."))
await self._remove_balance(ctx.author, price)
pets[name] = {
"type": pet_type,
"level": 1,
"xp": 0,
"happiness": 100,
"fed_at": ts_now(),
"bought_at": ts_now(),
}
await self._add_transaction(ctx.author, -price, f"Bought pet: {name}")
await ctx.send(embed=discord.Embed(
title=f"{pet_info['emoji']} New Pet!",
description=f"You adopted **{name}** the {pet_type}!",
colour=Clr.ECO,
))
async def _feed_pet(self, ctx: commands.Context, name: str):
async with self.eco_config.member(ctx.author).pets() as pets:
if name not in pets:
return await ctx.send(embed=err_embed(f"No pet named `{name}`."))
pet = pets[name]
pet["fed_at"] = ts_now()
pet["happiness"] = min(100, pet["happiness"] + 20)
pet["xp"] += 10
if pet["xp"] >= pet["level"] * 100:
pet["xp"] = 0
pet["level"] += 1
await ctx.send(embed=ok_embed(f"**{name}** leveled up to **{pet['level']}**! 🎉"))
pets[name] = pet
await ctx.send(embed=ok_embed(f"Fed **{name}**! Happiness: {pet['happiness']}%"))
async def _pet_collect(self, ctx: commands.Context):
"""Collect daily earnings from all pets."""
guild_data = await self.eco_config.guild(ctx.guild).all()
types = guild_data.get("pets", {}).get("types", {})
pets = await self.eco_config.member(ctx.author).pets()
if not pets:
return await ctx.send(embed=err_embed("You have no pets."))
total = 0
for name, pet in pets.items():
ptype = types.get(pet["type"], {})
base_earn = ptype.get("daily_earn", 10)
level_bonus = pet.get("level", 1) * 5
happiness_multi = pet.get("happiness", 50) / 100
earn = int((base_earn + level_bonus) * happiness_multi)
total += earn
if total > 0:
await self._add_balance(ctx.author, total)
await self._add_transaction(ctx.author, total, "Pet earnings")
await ctx.send(embed=ok_embed(
f"Your pets earned **{await self._format_amount(ctx.guild, total)}**! ({len(pets)} pets)"
))
else:
await ctx.send(embed=err_embed("Your pets didn't earn anything. Feed them!"))