EveCogs/nexuscore/economy.py
everestmcarthur 84459daf63 NexusCore v2.0.0 — 10 modules, 100+ commands, 8,132 lines
Major expansion:
- All 8 modules fully expanded with advanced features
- New: Embed Builder (webhook-based, templates, scheduling, variables)
- New: Dashboard Integration (web config for all modules)
- Serverlog: 30+ event types, invite tracking, per-event channels
- Moderation: quarantine, reputation, warning decay, staff leaderboard,
  anti-raid, anti-nuke, 6 automod modules, appeals, cross-server bans
- Economy: roulette, dice, fishing, mining, crafting, achievements,
  income roles, auction house, pets (6 types), heists, gambling stats
- Tickets: auto-close, tags, HTML transcripts, staff tracking, reopen
- Applications: staff voting, bulk actions, webhook notifications, templates
- Suggestions: edit, merge, 10 statuses, tag system, auto-approve/deny
- Reaction Roles: verification panels, exclusive groups, sticky/temp roles
- Giveaways: drop mode, bonus entries, templates, DM notifications
- All views use custom_id for persistence across restarts
- Full syntax verification on all 13 Python files
2026-05-08 02:38:34 +00:00

1128 lines
56 KiB
Python
Raw Permalink 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.

"""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")