EveCogs/nexuscore/economy.py

1129 lines
56 KiB
Python
Raw Permalink Normal View History

"""NexusCore — Economy v2: wallet+bank, daily/weekly streaks, work/crime/rob, coinflip/slots/blackjack,
roulette, dice, lottery, stock market, fishing, mining, crafting, achievements, income roles, auction house,
shop, heist, pets (6 types), gambling stats, leaderboard."""
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, parse_duration, safe_send, safe_dm, ConfirmView, Paginator, chunk_list,
)
# ── Defaults ───────────────────────────────────────────────────────────────
ECO_DEFAULTS_GUILD = {
"enabled": True,
"currency_name": "Coins",
"currency_emoji": "🪙",
"currency_symbol": "$",
"daily_amount": 100,
"daily_streak_bonus": 10,
"weekly_amount": 500,
"weekly_streak_bonus": 50,
"work_min": 50,
"work_max": 200,
"work_cooldown": 3600,
"crime_min": 100,
"crime_max": 500,
"crime_cooldown": 7200,
"crime_fail_chance": 40,
"crime_fine_pct": 30,
"rob_enabled": True,
"rob_cooldown": 14400,
"rob_min_wallet": 500,
"rob_fail_chance": 50,
"rob_max_pct": 50,
"tax_rate": 0,
"interest_rate": 2,
"interest_cap": 100000,
"max_bank": 0, # 0 = unlimited
"shop_items": {},
"log_channel": None,
"gambling_tax": 0,
"min_bet": 10,
"max_bet": 100000,
"slots_emojis": ["🍎", "🍊", "🍇", "🍒", "🍋", "💎", "7️⃣"],
"work_responses": [
"You worked as a programmer and earned {amount}!",
"You did some freelance work and earned {amount}!",
"You delivered pizzas and earned {amount}!",
"You mowed lawns and earned {amount}!",
"You walked dogs and earned {amount}!",
"You drove for a ride-share and earned {amount}!",
],
"crime_success_responses": [
"You robbed a gas station and got {amount}!",
"You hacked into a database and sold info for {amount}!",
"You pickpocketed someone for {amount}!",
],
"crime_fail_responses": [
"You got caught and fined {amount}!",
"The police caught you. You paid {amount} in bail!",
],
# Advanced gambling
"roulette_enabled": True,
"dice_enabled": True,
"lottery": {"enabled": False, "price": 100, "jackpot": 0, "last_draw": 0, "draw_interval": 86400, "tickets": {}},
# Stock market
"stocks": {"enabled": False, "market": {}, "history": {}},
# Gathering
"fishing": {
"enabled": False, "cooldown": 1800, "fish_types": {
"common_fish": {"emoji": "🐟", "min_value": 10, "max_value": 50, "weight": 50},
"rare_fish": {"emoji": "🐠", "min_value": 50, "max_value": 200, "weight": 25},
"legendary_fish": {"emoji": "🐡", "min_value": 200, "max_value": 1000, "weight": 5},
"treasure": {"emoji": "💰", "min_value": 500, "max_value": 2000, "weight": 2},
"junk": {"emoji": "🪣", "min_value": 1, "max_value": 10, "weight": 18},
}
},
"mining": {
"enabled": False, "cooldown": 2400, "ore_types": {
"coal": {"emoji": "⬛", "min_value": 5, "max_value": 20, "weight": 40},
"iron": {"emoji": "⬜", "min_value": 20, "max_value": 60, "weight": 30},
"gold": {"emoji": "🟡", "min_value": 50, "max_value": 200, "weight": 15},
"diamond": {"emoji": "💎", "min_value": 200, "max_value": 800, "weight": 5},
"emerald": {"emoji": "🟢", "min_value": 300, "max_value": 1000, "weight": 3},
"nothing": {"emoji": "💨", "min_value": 0, "max_value": 0, "weight": 7},
}
},
# Crafting
"crafting": {
"enabled": False, "recipes": {
"gold_bar": {"emoji": "🥇", "requires": {"gold": 3}, "value": 500, "description": "A shiny gold bar"},
"diamond_ring": {"emoji": "💍", "requires": {"diamond": 1, "gold": 1}, "value": 1500, "description": "A diamond ring"},
}
},
# Achievements
"achievements": {
"first_daily": {"name": "First Daily", "emoji": "📅", "description": "Claim your first daily", "reward": 50},
"big_spender": {"name": "Big Spender", "emoji": "💸", "description": "Spend 10,000 coins", "reward": 200},
"lucky_7": {"name": "Lucky Seven", "emoji": "🎰", "description": "Win slots with 7️⃣", "reward": 777},
"high_roller": {"name": "High Roller", "emoji": "🎲", "description": "Bet over 10,000 at once", "reward": 500},
"millionaire": {"name": "Millionaire", "emoji": "💰", "description": "Reach 1,000,000 total balance", "reward": 5000},
"fisherman": {"name": "Master Fisherman", "emoji": "🎣", "description": "Catch 50 fish", "reward": 300},
"miner": {"name": "Master Miner", "emoji": "⛏️", "description": "Mine 50 ores", "reward": 300},
},
# Income roles
"income_roles": {}, # role_id -> {amount, interval_hours}
# Auction
"auction": {"enabled": False, "listings": {}, "fee_pct": 5},
# Pets
"pets": {
"enabled": True,
"types": {
"cat": {"emoji": "🐱", "base_price": 500, "income": 10, "income_interval": 3600},
"dog": {"emoji": "🐶", "base_price": 500, "income": 10, "income_interval": 3600},
"bird": {"emoji": "🐦", "base_price": 300, "income": 7, "income_interval": 3600},
"fish": {"emoji": "🐟", "base_price": 200, "income": 5, "income_interval": 3600},
"dragon": {"emoji": "🐉", "base_price": 5000, "income": 50, "income_interval": 3600},
"unicorn": {"emoji": "🦄", "base_price": 10000, "income": 100, "income_interval": 3600},
},
},
# Heist
"heist": {"cooldown": 3600, "min_bet": 100, "max_players": 10, "vault_base": 10000},
}
ECO_DEFAULTS_MEMBER = {
"wallet": 0,
"bank": 0,
"last_daily": 0,
"daily_streak": 0,
"last_weekly": 0,
"weekly_streak": 0,
"last_work": 0,
"last_crime": 0,
"last_rob": 0,
"last_fish": 0,
"last_mine": 0,
"inventory": {},
"materials": {}, # material_name -> count
"pets": {},
"pet_last_collect": 0,
"pet_last_feed": 0,
"transactions": [],
"gambling_stats": {"won": 0, "lost": 0, "total_wagered": 0, "biggest_win": 0},
"achievements": [],
"total_earned": 0,
"total_spent": 0,
"fish_caught": 0,
"ores_mined": 0,
"stocks_owned": {}, # stock_name -> quantity
}
# ── Views ──────────────────────────────────────────────────────────────────
class ShopView(discord.ui.View):
def __init__(self, cog, guild, items):
super().__init__(timeout=120)
options = []
for iid, item in list(items.items())[:25]:
options.append(discord.SelectOption(
label=item["name"], value=iid,
description=f"{item['price']:,} coins" + (f" · {item.get('description', '')[:50]}" if item.get("description") else ""),
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.cog = cog
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)
class HeistJoinView(discord.ui.View):
def __init__(self, cog, heist_id: str, bet: int):
super().__init__(timeout=60)
self.cog = cog
self.heist_id = heist_id
self.bet = bet
self.participants = []
@discord.ui.button(label="🔫 Join Heist!", style=discord.ButtonStyle.danger, custom_id="nexus_eco_heist_join")
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 joined!", ephemeral=True)
wallet = await self.cog.eco_config.member(interaction.user).wallet()
if wallet < self.bet:
return await interaction.response.send_message("Not enough coins!", ephemeral=True)
self.participants.append(interaction.user.id)
await interaction.response.send_message(f"🔫 You've joined the heist! ({len(self.participants)} participants)", ephemeral=True)
class BlackjackView(discord.ui.View):
def __init__(self, cog, ctx, bet: int, player_hand: list, dealer_hand: list, deck: list):
super().__init__(timeout=60)
self.cog = cog
self.ctx = ctx
self.bet = bet
self.player_hand = player_hand
self.dealer_hand = dealer_hand
self.deck = deck
self.doubled = False
self.done = False
@discord.ui.button(label="Hit", style=discord.ButtonStyle.primary, emoji="🃏")
async def hit_btn(self, interaction: discord.Interaction, button: discord.ui.Button):
if interaction.user.id != self.ctx.author.id:
return
self.player_hand.append(self.deck.pop())
total = bj_value(self.player_hand)
if total > 21:
self.done = True
await self.cog._end_blackjack(interaction, self, "bust")
elif total == 21:
self.done = True
await self.cog._end_blackjack(interaction, self, "stand")
else:
embed = self.cog._bj_embed(self.player_hand, self.dealer_hand, self.bet, hidden=True)
await interaction.response.edit_message(embed=embed, view=self)
@discord.ui.button(label="Stand", style=discord.ButtonStyle.secondary, emoji="✋")
async def stand_btn(self, interaction: discord.Interaction, button: discord.ui.Button):
if interaction.user.id != self.ctx.author.id:
return
self.done = True
await self.cog._end_blackjack(interaction, self, "stand")
@discord.ui.button(label="Double Down", style=discord.ButtonStyle.danger, emoji="💰")
async def double_btn(self, interaction: discord.Interaction, button: discord.ui.Button):
if interaction.user.id != self.ctx.author.id:
return
wallet = await self.cog.eco_config.member(self.ctx.author).wallet()
if wallet < self.bet:
return await interaction.response.send_message("Not enough for double down.", ephemeral=True)
self.bet *= 2
self.doubled = True
self.player_hand.append(self.deck.pop())
self.done = True
await self.cog._end_blackjack(interaction, self, "stand")
class AuctionListView(discord.ui.View):
def __init__(self, cog, guild, listings):
super().__init__(timeout=120)
self.cog = cog
if not listings:
return
options = []
for lid, listing in list(listings.items())[:25]:
options.append(discord.SelectOption(
label=listing.get("item_name", "?"), value=lid,
description=f"Current bid: {listing.get('current_bid', listing.get('starting_price', 0)):,}",
))
self.sel = discord.ui.Select(placeholder="Bid on an item...", options=options)
self.sel.callback = self.on_select
self.add_item(self.sel)
async def on_select(self, interaction: discord.Interaction):
listing_id = self.sel.values[0]
modal = AuctionBidModal(self.cog, listing_id)
await interaction.response.send_modal(modal)
class AuctionBidModal(discord.ui.Modal):
def __init__(self, cog, listing_id: str):
super().__init__(title="Place a Bid")
self.cog = cog
self.listing_id = listing_id
self.amount = discord.ui.TextInput(label="Bid Amount", placeholder="1000", max_length=10)
self.add_item(self.amount)
async def on_submit(self, interaction: discord.Interaction):
try:
bid = int(self.amount.value)
except ValueError:
return await interaction.response.send_message("Invalid amount.", ephemeral=True)
await self.cog._place_bid(interaction, self.listing_id, bid)
# ── Blackjack helpers ──────────────────────────────────────────────────────
SUITS = ["♠", "♥", "♦", "♣"]
RANKS = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"]
def new_deck():
deck = [(r, s) for s in SUITS for r in RANKS]
random.shuffle(deck)
return deck
def card_str(hand):
return " ".join(f"`{r}{s}`" for r, s in hand)
def bj_value(hand):
val = 0
aces = 0
for r, s in hand:
if r in ("J", "Q", "K"):
val += 10
elif r == "A":
val += 11
aces += 1
else:
val += int(r)
while val > 21 and aces > 0:
val -= 10
aces -= 1
return val
# ── Mixin ──────────────────────────────────────────────────────────────────
class EconomyMixin:
"""Economy mixin — v2 with roulette, dice, lottery, stocks, fishing, mining,
crafting, achievements, income roles, auction, expanded gambling."""
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._active_heists = {}
# ── Balance helpers ────────────────────────────────────────────────────
async def _get_balance(self, member: discord.Member) -> tuple[int, int]:
w = await self.eco_config.member(member).wallet()
b = await self.eco_config.member(member).bank()
return w, b
async def _add_balance(self, member: discord.Member, amount: int, to_bank: bool = False):
if to_bank:
current = await self.eco_config.member(member).bank()
await self.eco_config.member(member).bank.set(current + amount)
else:
current = await self.eco_config.member(member).wallet()
await self.eco_config.member(member).wallet.set(current + amount)
current_total = await self.eco_config.member(member).total_earned()
await self.eco_config.member(member).total_earned.set(current_total + max(0, amount))
async def _remove_balance(self, member: discord.Member, amount: int, from_bank: bool = False):
if from_bank:
current = await self.eco_config.member(member).bank()
await self.eco_config.member(member).bank.set(max(0, current - amount))
else:
current = await self.eco_config.member(member).wallet()
await self.eco_config.member(member).wallet.set(max(0, current - amount))
current_total = await self.eco_config.member(member).total_spent()
await self.eco_config.member(member).total_spent.set(current_total + max(0, amount))
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, "description": description, "at": ts_now()})
if len(txns) > 100:
txns[:] = txns[-100:]
async def _format_amount(self, guild, amount: int) -> str:
data = await self.eco_config.guild(guild).all()
return f"{data['currency_emoji']} {amount:,}"
async def _check_achievement(self, member: discord.Member, achievement_id: str):
"""Check and award an achievement."""
current = await self.eco_config.member(member).achievements()
if achievement_id in current:
return
data = await self.eco_config.guild(member.guild).all()
ach = data.get("achievements", {}).get(achievement_id)
if not ach:
return
async with self.eco_config.member(member).achievements() as achs:
achs.append(achievement_id)
reward = ach.get("reward", 0)
if reward:
await self._add_balance(member, reward)
await safe_dm(member, embed=discord.Embed(
title=f"🏆 Achievement Unlocked!",
description=f"{ach.get('emoji', '🏆')} **{ach['name']}**\n{ach.get('description', '')}\nReward: {reward:,} coins",
colour=Clr.ECO))
# ── Daily/Weekly ───────────────────────────────────────────────────────
async def _daily(self, ctx):
member = ctx.author
data = await self.eco_config.guild(ctx.guild).all()
last = await self.eco_config.member(member).last_daily()
streak = await self.eco_config.member(member).daily_streak()
now = ts_now()
if now - last < 72000: # 20h to allow some flexibility
remaining = 72000 - (now - last)
return await ctx.send(embed=err_embed(f"Daily cooldown: {duration_str(remaining)}"))
if now - last < 172800: # 48h to maintain streak
streak += 1
else:
streak = 1
await self.eco_config.member(member).daily_streak.set(streak)
await self.eco_config.member(member).last_daily.set(now)
base = data["daily_amount"]
bonus = data.get("daily_streak_bonus", 10) * (streak - 1)
total = base + bonus
await self._add_balance(member, total)
await self._add_transaction(member, total, f"Daily (streak {streak})")
embed = discord.Embed(title="📅 Daily Reward", colour=Clr.ECO)
embed.description = f"You received **{await self._format_amount(ctx.guild, total)}**!"
if bonus > 0:
embed.add_field(name="Streak Bonus", value=f"+{bonus:,} (🔥 {streak} day streak)", inline=True)
await ctx.send(embed=embed)
await self._check_achievement(member, "first_daily")
async def _weekly(self, ctx):
member = ctx.author
data = await self.eco_config.guild(ctx.guild).all()
last = await self.eco_config.member(member).last_weekly()
streak = await self.eco_config.member(member).weekly_streak()
now = ts_now()
if now - last < 604800:
remaining = 604800 - (now - last)
return await ctx.send(embed=err_embed(f"Weekly cooldown: {duration_str(remaining)}"))
if now - last < 1209600:
streak += 1
else:
streak = 1
await self.eco_config.member(member).weekly_streak.set(streak)
await self.eco_config.member(member).last_weekly.set(now)
base = data["weekly_amount"]
bonus = data.get("weekly_streak_bonus", 50) * (streak - 1)
total = base + bonus
await self._add_balance(member, total)
await self._add_transaction(member, total, f"Weekly (streak {streak})")
embed = discord.Embed(title="📅 Weekly Reward", colour=Clr.ECO)
embed.description = f"You received **{await self._format_amount(ctx.guild, total)}**!"
if bonus:
embed.add_field(name="Streak Bonus", value=f"+{bonus:,} (🔥 {streak} week streak)", inline=True)
await ctx.send(embed=embed)
# ── Work/Crime/Rob ─────────────────────────────────────────────────────
async def _work(self, ctx):
member = ctx.author
data = await self.eco_config.guild(ctx.guild).all()
last = await self.eco_config.member(member).last_work()
if ts_now() - last < data["work_cooldown"]:
remaining = data["work_cooldown"] - (ts_now() - last)
return await ctx.send(embed=err_embed(f"Work cooldown: {duration_str(remaining)}"))
amount = random.randint(data["work_min"], data["work_max"])
await self.eco_config.member(member).last_work.set(ts_now())
await self._add_balance(member, amount)
await self._add_transaction(member, amount, "Work")
responses = data.get("work_responses", ["You worked and earned {amount}!"])
msg = random.choice(responses).format(amount=await self._format_amount(ctx.guild, amount))
await ctx.send(embed=ok_embed(msg))
async def _crime(self, ctx):
member = ctx.author
data = await self.eco_config.guild(ctx.guild).all()
last = await self.eco_config.member(member).last_crime()
if ts_now() - last < data["crime_cooldown"]:
remaining = data["crime_cooldown"] - (ts_now() - last)
return await ctx.send(embed=err_embed(f"Crime cooldown: {duration_str(remaining)}"))
await self.eco_config.member(member).last_crime.set(ts_now())
if random.randint(1, 100) <= data["crime_fail_chance"]:
fine = random.randint(data["crime_min"], data["crime_max"]) * data.get("crime_fine_pct", 30) // 100
await self._remove_balance(member, fine)
await self._add_transaction(member, -fine, "Crime (failed)")
responses = data.get("crime_fail_responses", ["You got caught and paid {amount}!"])
msg = random.choice(responses).format(amount=await self._format_amount(ctx.guild, fine))
await ctx.send(embed=err_embed(msg))
else:
amount = random.randint(data["crime_min"], data["crime_max"])
await self._add_balance(member, amount)
await self._add_transaction(member, amount, "Crime (success)")
responses = data.get("crime_success_responses", ["You got away with {amount}!"])
msg = random.choice(responses).format(amount=await self._format_amount(ctx.guild, amount))
await ctx.send(embed=ok_embed(msg))
async def _rob(self, ctx, target: discord.Member):
data = await self.eco_config.guild(ctx.guild).all()
if not 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("Can't rob yourself."))
if target.bot:
return await ctx.send(embed=err_embed("Can't rob bots."))
last = await self.eco_config.member(ctx.author).last_rob()
if ts_now() - last < data["rob_cooldown"]:
remaining = data["rob_cooldown"] - (ts_now() - last)
return await ctx.send(embed=err_embed(f"Rob cooldown: {duration_str(remaining)}"))
target_wallet = await self.eco_config.member(target).wallet()
if target_wallet < data["rob_min_wallet"]:
return await ctx.send(embed=err_embed(f"{target.display_name} doesn't have enough to rob."))
await self.eco_config.member(ctx.author).last_rob.set(ts_now())
if random.randint(1, 100) <= data["rob_fail_chance"]:
fine = min(target_wallet, random.randint(50, 200))
await self._remove_balance(ctx.author, fine)
await self._add_balance(target, fine)
await ctx.send(embed=err_embed(f"You got caught and paid {target.mention} {fine:,} coins."))
else:
max_rob = int(target_wallet * data["rob_max_pct"] / 100)
stolen = random.randint(1, max(1, max_rob))
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=ok_embed(f"You stole **{stolen:,}** from {target.mention}!"))
# ── Bank ───────────────────────────────────────────────────────────────
async def _deposit(self, ctx, amount: int):
wallet = await self.eco_config.member(ctx.author).wallet()
if amount > wallet or amount <= 0:
return await ctx.send(embed=err_embed("Invalid amount."))
data = await self.eco_config.guild(ctx.guild).all()
max_bank = data.get("max_bank", 0)
bank = await self.eco_config.member(ctx.author).bank()
if max_bank and bank + amount > max_bank:
amount = max_bank - bank
if amount <= 0:
return await ctx.send(embed=err_embed("Bank is full."))
await self._remove_balance(ctx.author, amount)
await self._add_balance(ctx.author, amount, to_bank=True)
await ctx.send(embed=ok_embed(f"Deposited **{amount:,}**"))
async def _withdraw(self, ctx, amount: int):
bank = await self.eco_config.member(ctx.author).bank()
if amount > bank or amount <= 0:
return await ctx.send(embed=err_embed("Invalid amount."))
await self._remove_balance(ctx.author, amount, from_bank=True)
await self._add_balance(ctx.author, amount)
await ctx.send(embed=ok_embed(f"Withdrew **{amount:,}**"))
# ── Gambling ───────────────────────────────────────────────────────────
async def _validate_bet(self, ctx, bet: int) -> bool:
data = await self.eco_config.guild(ctx.guild).all()
if bet < data["min_bet"]:
await ctx.send(embed=err_embed(f"Minimum bet: {data['min_bet']:,}"))
return False
if data["max_bet"] and bet > data["max_bet"]:
await ctx.send(embed=err_embed(f"Maximum bet: {data['max_bet']:,}"))
return False
wallet = await self.eco_config.member(ctx.author).wallet()
if bet > wallet:
await ctx.send(embed=err_embed("Not enough coins."))
return False
return True
async def _update_gambling_stats(self, member, won: bool, amount: int, wagered: int):
async with self.eco_config.member(member).gambling_stats() as stats:
if won:
stats["won"] = stats.get("won", 0) + amount
stats["biggest_win"] = max(stats.get("biggest_win", 0), amount)
else:
stats["lost"] = stats.get("lost", 0) + amount
stats["total_wagered"] = stats.get("total_wagered", 0) + wagered
if wagered >= 10000:
await self._check_achievement(member, "high_roller")
async def _coinflip(self, ctx, bet: int, choice: str):
if not await self._validate_bet(ctx, bet):
return
choice = choice.lower()
if choice not in ("heads", "tails", "h", "t"):
return await ctx.send(embed=err_embed("Choose `heads` or `tails`."))
result = random.choice(["heads", "tails"])
won = choice[0] == result[0]
emoji = "🪙"
if won:
await self._add_balance(ctx.author, bet)
await self._add_transaction(ctx.author, bet, "Coinflip (won)")
await self._update_gambling_stats(ctx.author, True, bet, bet)
embed = discord.Embed(title=f"{emoji} Coinflip — {result.title()}", description=f"You won **{bet:,}**! 🎉", colour=Clr.SUCCESS)
else:
await self._remove_balance(ctx.author, bet)
await self._add_transaction(ctx.author, -bet, "Coinflip (lost)")
await self._update_gambling_stats(ctx.author, False, bet, bet)
embed = discord.Embed(title=f"{emoji} Coinflip — {result.title()}", description=f"You lost **{bet:,}**.", colour=Clr.ERROR)
await ctx.send(embed=embed)
async def _slots(self, ctx, bet: int):
if not await self._validate_bet(ctx, bet):
return
data = await self.eco_config.guild(ctx.guild).all()
emojis = data.get("slots_emojis", ["🍎", "🍊", "🍇", "🍒", "🍋", "💎", "7️⃣"])
reels = [random.choice(emojis) for _ in range(3)]
display = f"{' | '.join(reels)}"
if reels[0] == reels[1] == reels[2]:
if reels[0] == "7️⃣":
mult = 10
await self._check_achievement(ctx.author, "lucky_7")
elif reels[0] == "💎":
mult = 5
else:
mult = 3
winnings = bet * mult
await self._add_balance(ctx.author, winnings)
await self._add_transaction(ctx.author, winnings, f"Slots ({mult}x)")
await self._update_gambling_stats(ctx.author, True, winnings, bet)
embed = discord.Embed(title="🎰 JACKPOT!", description=f"{display}\n\nYou won **{winnings:,}** ({mult}x)! 🎉", colour=Clr.SUCCESS)
elif reels[0] == reels[1] or reels[1] == reels[2]:
winnings = int(bet * 1.5)
await self._add_balance(ctx.author, winnings - bet)
await self._add_transaction(ctx.author, winnings - bet, "Slots (1.5x)")
await self._update_gambling_stats(ctx.author, True, winnings - bet, bet)
embed = discord.Embed(title="🎰 Slots", description=f"{display}\n\nTwo match! +**{winnings - bet:,}**", colour=Clr.ECO)
else:
await self._remove_balance(ctx.author, bet)
await self._add_transaction(ctx.author, -bet, "Slots (lost)")
await self._update_gambling_stats(ctx.author, False, bet, bet)
embed = discord.Embed(title="🎰 Slots", description=f"{display}\n\nNo match. You lost **{bet:,}**.", colour=Clr.ERROR)
await ctx.send(embed=embed)
async def _blackjack(self, ctx, bet: int):
if not await self._validate_bet(ctx, bet):
return
deck = new_deck()
player_hand = [deck.pop(), deck.pop()]
dealer_hand = [deck.pop(), deck.pop()]
await self._remove_balance(ctx.author, bet)
if bj_value(player_hand) == 21:
winnings = int(bet * 2.5)
await self._add_balance(ctx.author, winnings)
await self._add_transaction(ctx.author, winnings - bet, "Blackjack (natural)")
await self._update_gambling_stats(ctx.author, True, winnings - bet, bet)
embed = self._bj_embed(player_hand, dealer_hand, bet, hidden=False)
embed.title = "🃏 BLACKJACK!"
embed.description += f"\n\n🎉 Natural 21! You won **{winnings - bet:,}**!"
embed.colour = Clr.SUCCESS
return await ctx.send(embed=embed)
view = BlackjackView(self, ctx, bet, player_hand, dealer_hand, deck)
embed = self._bj_embed(player_hand, dealer_hand, bet, hidden=True)
await ctx.send(embed=embed, view=view)
def _bj_embed(self, player_hand, dealer_hand, bet, hidden=True):
dealer_show = f"{card_str([dealer_hand[0]])} `??`" if hidden else card_str(dealer_hand)
dealer_val = bj_value([dealer_hand[0]]) if hidden else bj_value(dealer_hand)
player_val = bj_value(player_hand)
embed = discord.Embed(title="🃏 Blackjack", colour=Clr.ECO)
embed.add_field(name=f"Dealer ({dealer_val}{'?' if hidden else ''})", value=dealer_show, inline=False)
embed.add_field(name=f"You ({player_val})", value=card_str(player_hand), inline=False)
embed.set_footer(text=f"Bet: {bet:,}")
return embed
async def _end_blackjack(self, interaction, view: BlackjackView, action: str):
bet = view.bet
player_hand = view.player_hand
dealer_hand = view.dealer_hand
deck = view.deck
player_val = bj_value(player_hand)
if action == "bust":
await self._add_transaction(view.ctx.author, -bet, "Blackjack (bust)")
await self._update_gambling_stats(view.ctx.author, False, bet, bet)
embed = self._bj_embed(player_hand, dealer_hand, bet, hidden=False)
embed.description = f"💥 Bust! ({player_val}) You lost **{bet:,}**."
embed.colour = Clr.ERROR
try:
await interaction.response.edit_message(embed=embed, view=None)
except Exception:
pass
return
# Dealer plays
while bj_value(dealer_hand) < 17:
dealer_hand.append(deck.pop())
dealer_val = bj_value(dealer_hand)
embed = self._bj_embed(player_hand, dealer_hand, bet, hidden=False)
if dealer_val > 21 or player_val > dealer_val:
winnings = bet * 2
await self._add_balance(view.ctx.author, winnings)
await self._add_transaction(view.ctx.author, winnings - bet, "Blackjack (won)")
await self._update_gambling_stats(view.ctx.author, True, winnings - bet, bet)
embed.description = f"🎉 You won **{winnings - bet:,}**!"
embed.colour = Clr.SUCCESS
elif player_val == dealer_val:
await self._add_balance(view.ctx.author, bet)
embed.description = "🤝 Push! Bet returned."
embed.colour = Clr.ECO
else:
await self._add_transaction(view.ctx.author, -bet, "Blackjack (lost)")
await self._update_gambling_stats(view.ctx.author, False, bet, bet)
embed.description = f"💔 Dealer wins. You lost **{bet:,}**."
embed.colour = Clr.ERROR
try:
await interaction.response.edit_message(embed=embed, view=None)
except Exception:
pass
# ── Roulette ───────────────────────────────────────────────────────────
async def _roulette(self, ctx, bet: int, choice: str):
if not await self._validate_bet(ctx, bet):
return
result = random.randint(0, 36)
is_red = result in [1, 3, 5, 7, 9, 12, 14, 16, 18, 19, 21, 23, 25, 27, 30, 32, 34, 36]
is_black = result != 0 and not is_red
colour = "🔴" if is_red else ("⚫" if is_black else "🟢")
multiplier = 0
choice_lower = choice.lower()
if choice_lower == "red" and is_red:
multiplier = 2
elif choice_lower == "black" and is_black:
multiplier = 2
elif choice_lower in ("even", "odd"):
if result != 0:
if (choice_lower == "even" and result % 2 == 0) or (choice_lower == "odd" and result % 2 == 1):
multiplier = 2
elif choice_lower in ("high", "low"):
if result != 0:
if (choice_lower == "low" and 1 <= result <= 18) or (choice_lower == "high" and 19 <= result <= 36):
multiplier = 2
else:
try:
num = int(choice)
if num == result:
multiplier = 36
except ValueError:
return await ctx.send(embed=err_embed("Choose: red, black, even, odd, high, low, or a number 0-36"))
if multiplier:
winnings = bet * multiplier
await self._add_balance(ctx.author, winnings - bet)
await self._add_transaction(ctx.author, winnings - bet, f"Roulette ({multiplier}x)")
await self._update_gambling_stats(ctx.author, True, winnings - bet, bet)
embed = discord.Embed(title=f"🎰 Roulette — {colour} {result}", description=f"You won **{winnings:,}** ({multiplier}x)! 🎉", colour=Clr.SUCCESS)
else:
await self._remove_balance(ctx.author, bet)
await self._add_transaction(ctx.author, -bet, "Roulette (lost)")
await self._update_gambling_stats(ctx.author, False, bet, bet)
embed = discord.Embed(title=f"🎰 Roulette — {colour} {result}", description=f"You lost **{bet:,}**.", colour=Clr.ERROR)
await ctx.send(embed=embed)
# ── Dice ───────────────────────────────────────────────────────────────
async def _dice(self, ctx, bet: int, guess: int):
if not await self._validate_bet(ctx, bet):
return
if not 2 <= guess <= 12:
return await ctx.send(embed=err_embed("Guess a total between 2-12."))
d1, d2 = random.randint(1, 6), random.randint(1, 6)
total = d1 + d2
if total == guess:
odds_map = {2: 36, 3: 18, 4: 12, 5: 9, 6: 7, 7: 6, 8: 7, 9: 9, 10: 12, 11: 18, 12: 36}
mult = odds_map.get(guess, 6)
winnings = bet * mult
await self._add_balance(ctx.author, winnings - bet)
await self._add_transaction(ctx.author, winnings - bet, f"Dice ({mult}x)")
await self._update_gambling_stats(ctx.author, True, winnings - bet, bet)
embed = discord.Embed(title=f"🎲 {d1} + {d2} = {total}", description=f"🎉 Exact match! Won **{winnings:,}** ({mult}x)", colour=Clr.SUCCESS)
else:
await self._remove_balance(ctx.author, bet)
await self._add_transaction(ctx.author, -bet, "Dice (lost)")
await self._update_gambling_stats(ctx.author, False, bet, bet)
embed = discord.Embed(title=f"🎲 {d1} + {d2} = {total}", description=f"You guessed {guess}. Lost **{bet:,}**.", colour=Clr.ERROR)
await ctx.send(embed=embed)
# ── Fishing ────────────────────────────────────────────────────────────
async def _fish(self, ctx):
data = await self.eco_config.guild(ctx.guild).all()
fishing = data.get("fishing", {})
if not fishing.get("enabled"):
return await ctx.send(embed=err_embed("Fishing is disabled."))
last = await self.eco_config.member(ctx.author).last_fish()
if ts_now() - last < fishing.get("cooldown", 1800):
remaining = fishing.get("cooldown", 1800) - (ts_now() - last)
return await ctx.send(embed=err_embed(f"Fishing cooldown: {duration_str(remaining)}"))
await self.eco_config.member(ctx.author).last_fish.set(ts_now())
types = fishing.get("fish_types", {})
weights = [v["weight"] for v in types.values()]
names = list(types.keys())
caught_name = random.choices(names, weights=weights, k=1)[0]
caught = types[caught_name]
value = random.randint(caught["min_value"], caught["max_value"])
await self._add_balance(ctx.author, value)
await self._add_transaction(ctx.author, value, f"Fishing ({caught_name})")
count = await self.eco_config.member(ctx.author).fish_caught()
await self.eco_config.member(ctx.author).fish_caught.set(count + 1)
if count + 1 >= 50:
await self._check_achievement(ctx.author, "fisherman")
embed = discord.Embed(
title="🎣 Fishing",
description=f"You caught a {caught['emoji']} **{caught_name.replace('_', ' ').title()}**!\nSold for **{value:,}** coins.",
colour=Clr.ECO,
)
await ctx.send(embed=embed)
# ── Mining ─────────────────────────────────────────────────────────────
async def _mine(self, ctx):
data = await self.eco_config.guild(ctx.guild).all()
mining = data.get("mining", {})
if not mining.get("enabled"):
return await ctx.send(embed=err_embed("Mining is disabled."))
last = await self.eco_config.member(ctx.author).last_mine()
if ts_now() - last < mining.get("cooldown", 2400):
remaining = mining.get("cooldown", 2400) - (ts_now() - last)
return await ctx.send(embed=err_embed(f"Mining cooldown: {duration_str(remaining)}"))
await self.eco_config.member(ctx.author).last_mine.set(ts_now())
types = mining.get("ore_types", {})
weights = [v["weight"] for v in types.values()]
names = list(types.keys())
mined_name = random.choices(names, weights=weights, k=1)[0]
mined = types[mined_name]
value = random.randint(mined["min_value"], mined["max_value"])
# Store materials for crafting
if mined_name != "nothing":
async with self.eco_config.member(ctx.author).materials() as mats:
mats[mined_name] = mats.get(mined_name, 0) + 1
await self._add_balance(ctx.author, value)
await self._add_transaction(ctx.author, value, f"Mining ({mined_name})")
count = await self.eco_config.member(ctx.author).ores_mined()
await self.eco_config.member(ctx.author).ores_mined.set(count + 1)
if count + 1 >= 50:
await self._check_achievement(ctx.author, "miner")
embed = discord.Embed(
title="⛏️ Mining",
description=f"You mined {mined['emoji']} **{mined_name.replace('_', ' ').title()}**!"
+ (f"\nSold for **{value:,}** coins." if value else "\nNothing valuable..."),
colour=Clr.ECO,
)
await ctx.send(embed=embed)
# ── Crafting ───────────────────────────────────────────────────────────
async def _craft(self, ctx, recipe_name: str):
data = await self.eco_config.guild(ctx.guild).all()
crafting = data.get("crafting", {})
if not crafting.get("enabled"):
return await ctx.send(embed=err_embed("Crafting is disabled."))
recipe = crafting.get("recipes", {}).get(recipe_name.lower())
if not recipe:
recipes = crafting.get("recipes", {})
recipe_list = "\n".join(f"{v.get('emoji', '📦')} **{k}** — requires: {', '.join(f'{c}x {m}' for m, c in v['requires'].items())}" for k, v in recipes.items())
return await ctx.send(embed=info_embed(f"Available recipes:\n{recipe_list}"))
materials = await self.eco_config.member(ctx.author).materials()
for mat, count in recipe["requires"].items():
if materials.get(mat, 0) < count:
return await ctx.send(embed=err_embed(f"Need {count}x {mat}, you have {materials.get(mat, 0)}."))
async with self.eco_config.member(ctx.author).materials() as mats:
for mat, count in recipe["requires"].items():
mats[mat] -= count
value = recipe.get("value", 0)
await self._add_balance(ctx.author, value)
await self._add_transaction(ctx.author, value, f"Crafted {recipe_name}")
embed = discord.Embed(
title="🔨 Crafted!",
description=f"You crafted {recipe.get('emoji', '📦')} **{recipe_name.replace('_', ' ').title()}**!\n{recipe.get('description', '')}\nValue: **{value:,}** coins",
colour=Clr.ECO,
)
await ctx.send(embed=embed)
# ── Shop ───────────────────────────────────────────────────────────────
async def _buy_item(self, interaction, item_id: str):
guild = interaction.guild
data = await self.eco_config.guild(guild).all()
item = 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)
if item.get("stock", -1) == 0:
return await interaction.response.send_message("Out of stock.", ephemeral=True)
inv = await self.eco_config.member(interaction.user).inventory()
if item.get("max_per_user", 0) > 0:
if inv.get(item_id, 0) >= item["max_per_user"]:
return await interaction.response.send_message("You already own the max.", 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(guild).shop_items() as items:
if item_id in items:
items[item_id]["stock"] -= 1
# Role item
if item.get("type") == "role" and item.get("role_id"):
role = 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']}")
await interaction.response.send_message(f"✅ Purchased **{item['name']}** for {item['price']:,}!", ephemeral=True)
total_spent = await self.eco_config.member(interaction.user).total_spent()
if total_spent >= 10000:
await self._check_achievement(interaction.user, "big_spender")
# ── Pets ───────────────────────────────────────────────────────────────
async def _buy_pet(self, ctx, pet_type: str, name: str):
data = await self.eco_config.guild(ctx.guild).all()
pets_config = data.get("pets", {})
if not pets_config.get("enabled"):
return await ctx.send(embed=err_embed("Pets are disabled."))
types = pets_config.get("types", {})
if pet_type not in types:
return await ctx.send(embed=err_embed(f"Available types: {', '.join(types.keys())}"))
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"Not enough! Need {price:,}."))
pets = await self.eco_config.member(ctx.author).pets()
if len(pets) >= 5:
return await ctx.send(embed=err_embed("Max 5 pets."))
if name.lower() in [n.lower() for n in pets]:
return await ctx.send(embed=err_embed("You already have a pet with that name."))
await self._remove_balance(ctx.author, price)
async with self.eco_config.member(ctx.author).pets() as p:
p[name] = {"type": pet_type, "level": 1, "xp": 0, "happiness": 100, "last_feed": 0}
await ctx.send(embed=ok_embed(f"{pet_info['emoji']} **{name}** the {pet_type} has joined you!"))
async def _feed_pet(self, ctx, name: str):
pets = await self.eco_config.member(ctx.author).pets()
if name not in pets:
return await ctx.send(embed=err_embed("Pet not found."))
pet = pets[name]
if ts_now() - pet.get("last_feed", 0) < 3600:
return await ctx.send(embed=err_embed("Pet isn't hungry yet."))
cost = 50 * pet.get("level", 1)
wallet = await self.eco_config.member(ctx.author).wallet()
if wallet < cost:
return await ctx.send(embed=err_embed(f"Need {cost:,} to feed."))
await self._remove_balance(ctx.author, cost)
async with self.eco_config.member(ctx.author).pets() as p:
if name in p:
p[name]["happiness"] = min(100, p[name].get("happiness", 50) + 20)
p[name]["xp"] = p[name].get("xp", 0) + 10
p[name]["last_feed"] = ts_now()
if p[name]["xp"] >= p[name].get("level", 1) * 50:
p[name]["level"] += 1
p[name]["xp"] = 0
await ctx.send(embed=ok_embed(f"🎉 **{name}** leveled up to Lv.{p[name]['level']}!"))
await ctx.send(embed=ok_embed(f"Fed **{name}**! (-{cost:,} coins, ❤️ +20%)"))
async def _pet_collect(self, ctx):
data = await self.eco_config.guild(ctx.guild).all()
pets_config = data.get("pets", {})
types = pets_config.get("types", {})
pets = await self.eco_config.member(ctx.author).pets()
last_collect = await self.eco_config.member(ctx.author).pet_last_collect()
if ts_now() - last_collect < 3600:
remaining = 3600 - (ts_now() - last_collect)
return await ctx.send(embed=err_embed(f"Collect cooldown: {duration_str(remaining)}"))
total = 0
breakdown = []
for name, pet in pets.items():
pt = types.get(pet["type"], {})
base = pt.get("income", 10)
income = base * pet.get("level", 1) * (pet.get("happiness", 50) / 100)
income = int(income)
if income > 0:
total += income
breakdown.append(f"{pt.get('emoji', '🐾')} **{name}** — +{income:,}")
if not total:
return await ctx.send(embed=err_embed("No pet income to collect."))
await self._add_balance(ctx.author, total)
await self.eco_config.member(ctx.author).pet_last_collect.set(ts_now())
await self._add_transaction(ctx.author, total, "Pet income")
embed = discord.Embed(title="🐾 Pet Income Collected!", colour=Clr.ECO)
embed.description = f"Total: **{total:,}**\n" + "\n".join(breakdown)
await ctx.send(embed=embed)
# ── Heist ──────────────────────────────────────────────────────────────
async def _start_heist(self, ctx, bet: int):
if not await self._validate_bet(ctx, bet):
return
gid = ctx.guild.id
if gid in self._active_heists:
return await ctx.send(embed=err_embed("A heist is already in progress!"))
data = await self.eco_config.guild(ctx.guild).all()
heist_config = data.get("heist", {})
min_bet = heist_config.get("min_bet", 100)
if bet < min_bet:
return await ctx.send(embed=err_embed(f"Minimum heist bet: {min_bet:,}"))
heist_id = short_id(8)
self._active_heists[gid] = heist_id
view = HeistJoinView(self, heist_id, bet)
view.participants = [ctx.author.id]
await self._remove_balance(ctx.author, bet)
embed = discord.Embed(
title="🔫 Heist Starting!", colour=Clr.ECO,
description=f"{ctx.author.mention} is planning a heist!\nBet: **{bet:,}** coins\nClick to join! (60s to join)",
)
msg = await ctx.send(embed=embed, view=view)
await asyncio.sleep(60)
view.stop()
participants = view.participants
for uid in participants:
if uid != ctx.author.id:
member = ctx.guild.get_member(uid)
if member:
await self._remove_balance(member, bet)
vault = heist_config.get("vault_base", 10000) + (bet * len(participants))
success_chance = min(90, 30 + (len(participants) * 10))
success = random.randint(1, 100) <= success_chance
if success:
share = vault // len(participants)
winners = []
for uid in participants:
member = ctx.guild.get_member(uid)
if member:
await self._add_balance(member, share)
await self._add_transaction(member, share - bet, "Heist (won)")
winners.append(member.mention)
embed = discord.Embed(
title="🔫 Heist Successful!", colour=Clr.SUCCESS,
description=f"The crew got away with **{vault:,}** coins!\nEach member gets **{share:,}**\n\nParticipants: {', '.join(winners)}",
)
else:
losers = []
for uid in participants:
member = ctx.guild.get_member(uid)
if member:
await self._add_transaction(member, -bet, "Heist (failed)")
losers.append(member.mention)
embed = discord.Embed(
title="🔫 Heist Failed!", colour=Clr.ERROR,
description=f"The police caught the crew! Everyone lost **{bet:,}** coins.\n\nParticipants: {', '.join(losers)}",
)
del self._active_heists[gid]
await ctx.send(embed=embed)
# ── Auction ────────────────────────────────────────────────────────────
async def _create_auction(self, ctx, item_name: str, starting_price: int, duration: int):
data = await self.eco_config.guild(ctx.guild).all()
if not data.get("auction", {}).get("enabled"):
return await ctx.send(embed=err_embed("Auction house is disabled."))
listing_id = short_id(8)
listing = {
"seller_id": ctx.author.id, "item_name": item_name,
"starting_price": starting_price, "current_bid": starting_price,
"highest_bidder": None, "ends_at": ts_now() + duration,
"ended": False,
}
async with self.eco_config.guild(ctx.guild).auction() as auction:
auction.setdefault("listings", {})[listing_id] = listing
await ctx.send(embed=ok_embed(f"Auction listed: **{item_name}** starting at {starting_price:,} (ID: `{listing_id}`)"))
async def _place_bid(self, interaction, listing_id: str, bid: int):
guild = interaction.guild
data = await self.eco_config.guild(guild).all()
listing = data.get("auction", {}).get("listings", {}).get(listing_id)
if not listing:
return await interaction.response.send_message("Listing not found.", ephemeral=True)
if listing.get("ended"):
return await interaction.response.send_message("Auction ended.", ephemeral=True)
if bid <= listing.get("current_bid", 0):
return await interaction.response.send_message(f"Bid must be higher than {listing['current_bid']:,}.", ephemeral=True)
wallet = await self.eco_config.member(interaction.user).wallet()
if wallet < bid:
return await interaction.response.send_message("Not enough coins.", ephemeral=True)
# Refund previous bidder
prev = listing.get("highest_bidder")
if prev:
prev_member = guild.get_member(prev)
if prev_member:
await self._add_balance(prev_member, listing["current_bid"])
await self._remove_balance(interaction.user, bid)
async with self.eco_config.guild(guild).auction() as auction:
if listing_id in auction.get("listings", {}):
auction["listings"][listing_id]["current_bid"] = bid
auction["listings"][listing_id]["highest_bidder"] = interaction.user.id
await interaction.response.send_message(f"Bid of **{bid:,}** placed!", ephemeral=True)
# ── Income roles loop ──────────────────────────────────────────────────
async def _income_role_loop(self):
"""Give passive income based on roles."""
while True:
try:
for guild in self.bot.guilds:
data = await self.eco_config.guild(guild).all()
income_roles = data.get("income_roles", {})
for role_id_str, config in income_roles.items():
role = guild.get_role(int(role_id_str))
if not role:
continue
for member in role.members:
if not member.bot:
await self._add_balance(member, config["amount"])
except Exception:
pass
await asyncio.sleep(3600) # Check every hour
# ── Millionaire check ──────────────────────────────────────────────────
async def _check_millionaire(self, member: discord.Member):
w, b = await self._get_balance(member)
if w + b >= 1000000:
await self._check_achievement(member, "millionaire")