forked from EllieBotDevs/elliebot
1032 lines
32 KiB
C#
1032 lines
32 KiB
C#
|
#nullable disable
|
|||
|
using LinqToDB;
|
|||
|
using LinqToDB.EntityFrameworkCore;
|
|||
|
using EllieBot.Db;
|
|||
|
using EllieBot.Db.Models;
|
|||
|
using EllieBot.Modules.Gambling.Bank;
|
|||
|
using EllieBot.Modules.Gambling.Common;
|
|||
|
using EllieBot.Modules.Gambling.Services;
|
|||
|
using EllieBot.Modules.Utility.Services;
|
|||
|
using EllieBot.Services.Currency;
|
|||
|
using System.Collections.Immutable;
|
|||
|
using System.Globalization;
|
|||
|
using System.Text;
|
|||
|
using EllieBot.Modules.Gambling.Rps;
|
|||
|
using EllieBot.Common.TypeReaders;
|
|||
|
using EllieBot.Modules.Patronage;
|
|||
|
|
|||
|
namespace EllieBot.Modules.Gambling;
|
|||
|
|
|||
|
public partial class Gambling : GamblingModule<GamblingService>
|
|||
|
{
|
|||
|
private readonly IGamblingService _gs;
|
|||
|
private readonly DbService _db;
|
|||
|
private readonly ICurrencyService _cs;
|
|||
|
private readonly DiscordSocketClient _client;
|
|||
|
private readonly NumberFormatInfo _enUsCulture;
|
|||
|
private readonly DownloadTracker _tracker;
|
|||
|
private readonly GamblingConfigService _configService;
|
|||
|
private readonly IBankService _bank;
|
|||
|
private readonly IPatronageService _ps;
|
|||
|
private readonly IRemindService _remind;
|
|||
|
private readonly GamblingTxTracker _gamblingTxTracker;
|
|||
|
|
|||
|
private IUserMessage rdMsg;
|
|||
|
|
|||
|
public Gambling(
|
|||
|
IGamblingService gs,
|
|||
|
DbService db,
|
|||
|
ICurrencyService currency,
|
|||
|
DiscordSocketClient client,
|
|||
|
DownloadTracker tracker,
|
|||
|
GamblingConfigService configService,
|
|||
|
IBankService bank,
|
|||
|
IPatronageService ps,
|
|||
|
IRemindService remind,
|
|||
|
GamblingTxTracker gamblingTxTracker)
|
|||
|
: base(configService)
|
|||
|
{
|
|||
|
_gs = gs;
|
|||
|
_db = db;
|
|||
|
_cs = currency;
|
|||
|
_client = client;
|
|||
|
_bank = bank;
|
|||
|
_ps = ps;
|
|||
|
_remind = remind;
|
|||
|
_gamblingTxTracker = gamblingTxTracker;
|
|||
|
|
|||
|
_enUsCulture = new CultureInfo("en-US", false).NumberFormat;
|
|||
|
_enUsCulture.NumberDecimalDigits = 0;
|
|||
|
_enUsCulture.NumberGroupSeparator = " ";
|
|||
|
_tracker = tracker;
|
|||
|
_configService = configService;
|
|||
|
}
|
|||
|
|
|||
|
public async Task<string> GetBalanceStringAsync(ulong userId)
|
|||
|
{
|
|||
|
var bal = await _cs.GetBalanceAsync(userId);
|
|||
|
return N(bal);
|
|||
|
}
|
|||
|
|
|||
|
[Cmd]
|
|||
|
public async Task BetStats()
|
|||
|
{
|
|||
|
var stats = await _gamblingTxTracker.GetAllAsync();
|
|||
|
|
|||
|
var eb = _sender.CreateEmbed()
|
|||
|
.WithOkColor();
|
|||
|
|
|||
|
var str = "` Feature `|` Bet `|`Paid Out`|` RoI `\n";
|
|||
|
str += "――――――――――――――――――――\n";
|
|||
|
foreach (var stat in stats)
|
|||
|
{
|
|||
|
var perc = (stat.PaidOut / stat.Bet).ToString("P2", Culture);
|
|||
|
str += $"`{stat.Feature.PadBoth(9)}`"
|
|||
|
+ $"|`{stat.Bet.ToString("N0").PadLeft(8, ' ')}`"
|
|||
|
+ $"|`{stat.PaidOut.ToString("N0").PadLeft(8, ' ')}`"
|
|||
|
+ $"|`{perc.PadLeft(6, ' ')}`\n";
|
|||
|
}
|
|||
|
|
|||
|
var bet = stats.Sum(x => x.Bet);
|
|||
|
var paidOut = stats.Sum(x => x.PaidOut);
|
|||
|
|
|||
|
if (bet == 0)
|
|||
|
bet = 1;
|
|||
|
|
|||
|
var tPerc = (paidOut / bet).ToString("P2", Culture);
|
|||
|
str += "――――――――――――――――――――\n";
|
|||
|
str += $"` {("TOTAL").PadBoth(7)}` "
|
|||
|
+ $"|**{N(bet).PadLeft(8, ' ')}**"
|
|||
|
+ $"|**{N(paidOut).PadLeft(8, ' ')}**"
|
|||
|
+ $"|`{tPerc.PadLeft(6, ' ')}`";
|
|||
|
|
|||
|
eb.WithDescription(str);
|
|||
|
|
|||
|
await Response().Embed(eb).SendAsync();
|
|||
|
}
|
|||
|
|
|||
|
[Cmd]
|
|||
|
public async Task Economy()
|
|||
|
{
|
|||
|
var ec = await _service.GetEconomyAsync();
|
|||
|
decimal onePercent = 0;
|
|||
|
|
|||
|
// This stops the top 1% from owning more than 100% of the money
|
|||
|
if (ec.Cash > 0)
|
|||
|
{
|
|||
|
onePercent = ec.OnePercent / (ec.Cash - ec.Bot);
|
|||
|
}
|
|||
|
|
|||
|
// [21:03] Bob Page: Kinda remids me of US economy
|
|||
|
var embed = _sender.CreateEmbed()
|
|||
|
.WithTitle(GetText(strs.economy_state))
|
|||
|
.AddField(GetText(strs.currency_owned), N(ec.Cash - ec.Bot))
|
|||
|
.AddField(GetText(strs.currency_one_percent), (onePercent * 100).ToString("F2") + "%")
|
|||
|
.AddField(GetText(strs.currency_planted), N(ec.Planted))
|
|||
|
.AddField(GetText(strs.owned_waifus_total), N(ec.Waifus))
|
|||
|
.AddField(GetText(strs.bot_currency), N(ec.Bot))
|
|||
|
.AddField(GetText(strs.bank_accounts), N(ec.Bank))
|
|||
|
.AddField(GetText(strs.total), N(ec.Cash + ec.Planted + ec.Waifus + ec.Bank))
|
|||
|
.WithOkColor();
|
|||
|
|
|||
|
// ec.Cash already contains ec.Bot as it's the total of all values in the CurrencyAmount column of the DiscordUser table
|
|||
|
await Response().Embed(embed).SendAsync();
|
|||
|
}
|
|||
|
|
|||
|
private static readonly FeatureLimitKey _timelyKey = new FeatureLimitKey()
|
|||
|
{
|
|||
|
Key = "timely:extra_percent",
|
|||
|
PrettyName = "Timely"
|
|||
|
};
|
|||
|
|
|||
|
private async Task RemindTimelyAction(SocketMessageComponent smc, DateTime when)
|
|||
|
{
|
|||
|
var tt = TimestampTag.FromDateTime(when, TimestampTagStyles.Relative);
|
|||
|
|
|||
|
await _remind.AddReminderAsync(ctx.User.Id,
|
|||
|
ctx.User.Id,
|
|||
|
ctx.Guild?.Id,
|
|||
|
true,
|
|||
|
when,
|
|||
|
GetText(strs.timely_time),
|
|||
|
ReminderType.Timely);
|
|||
|
|
|||
|
await smc.RespondConfirmAsync(_sender, GetText(strs.remind_timely(tt)), ephemeral: true);
|
|||
|
}
|
|||
|
|
|||
|
private EllieInteractionBase CreateRemindMeInteraction(int period)
|
|||
|
=> _inter
|
|||
|
.Create(ctx.User.Id,
|
|||
|
new ButtonBuilder(
|
|||
|
label: "Remind me",
|
|||
|
emote: Emoji.Parse("⏰"),
|
|||
|
customId: "timely:remind_me"),
|
|||
|
(smc) => RemindTimelyAction(smc, DateTime.UtcNow.Add(TimeSpan.FromHours(period)))
|
|||
|
);
|
|||
|
|
|||
|
[Cmd]
|
|||
|
public async Task Timely()
|
|||
|
{
|
|||
|
var val = Config.Timely.Amount;
|
|||
|
var period = Config.Timely.Cooldown;
|
|||
|
if (val <= 0 || period <= 0)
|
|||
|
{
|
|||
|
await Response().Error(strs.timely_none).SendAsync();
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
var inter = CreateRemindMeInteraction(period);
|
|||
|
|
|||
|
if (await _service.ClaimTimelyAsync(ctx.User.Id, period) is { } rem)
|
|||
|
{
|
|||
|
// Removes timely button if there is a timely reminder in DB
|
|||
|
if (_service.UserHasTimelyReminder(ctx.User.Id))
|
|||
|
{
|
|||
|
inter = null;
|
|||
|
}
|
|||
|
|
|||
|
var now = DateTime.UtcNow;
|
|||
|
var relativeTag = TimestampTag.FromDateTime(now.Add(rem), TimestampTagStyles.Relative);
|
|||
|
await Response().Pending(strs.timely_already_claimed(relativeTag)).Interaction(inter).SendAsync();
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
var result = await _ps.TryGetFeatureLimitAsync(_timelyKey, ctx.User.Id, 0);
|
|||
|
|
|||
|
val = (int)(val * (1 + (result.Quota! * 0.01f)));
|
|||
|
|
|||
|
await _cs.AddAsync(ctx.User.Id, val, new("timely", "claim"));
|
|||
|
|
|||
|
await Response().Confirm(strs.timely(N(val), period)).Interaction(inter).SendAsync();
|
|||
|
}
|
|||
|
|
|||
|
[Cmd]
|
|||
|
[OwnerOnly]
|
|||
|
public async Task TimelyReset()
|
|||
|
{
|
|||
|
await _service.RemoveAllTimelyClaimsAsync();
|
|||
|
await Response().Confirm(strs.timely_reset).SendAsync();
|
|||
|
}
|
|||
|
|
|||
|
[Cmd]
|
|||
|
[OwnerOnly]
|
|||
|
public async Task TimelySet(int amount, int period = 24)
|
|||
|
{
|
|||
|
if (amount < 0 || period < 0)
|
|||
|
{
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
_configService.ModifyConfig(gs =>
|
|||
|
{
|
|||
|
gs.Timely.Amount = amount;
|
|||
|
gs.Timely.Cooldown = period;
|
|||
|
});
|
|||
|
|
|||
|
if (amount == 0)
|
|||
|
{
|
|||
|
await Response().Confirm(strs.timely_set_none).SendAsync();
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
await Response()
|
|||
|
.Confirm(strs.timely_set(Format.Bold(N(amount)), Format.Bold(period.ToString())))
|
|||
|
.SendAsync();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
[Cmd]
|
|||
|
[RequireContext(ContextType.Guild)]
|
|||
|
public async Task Raffle([Leftover] IRole role = null)
|
|||
|
{
|
|||
|
role ??= ctx.Guild.EveryoneRole;
|
|||
|
|
|||
|
var members = (await role.GetMembersAsync()).Where(u => u.Status != UserStatus.Offline);
|
|||
|
var membersArray = members as IUser[] ?? members.ToArray();
|
|||
|
if (membersArray.Length == 0)
|
|||
|
{
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
var usr = membersArray[new EllieRandom().Next(0, membersArray.Length)];
|
|||
|
await Response()
|
|||
|
.Confirm("🎟 " + GetText(strs.raffled_user),
|
|||
|
$"**{usr.Username}**",
|
|||
|
footer: $"ID: {usr.Id}")
|
|||
|
.SendAsync();
|
|||
|
}
|
|||
|
|
|||
|
[Cmd]
|
|||
|
[RequireContext(ContextType.Guild)]
|
|||
|
public async Task RaffleAny([Leftover] IRole role = null)
|
|||
|
{
|
|||
|
role ??= ctx.Guild.EveryoneRole;
|
|||
|
|
|||
|
var members = await role.GetMembersAsync();
|
|||
|
var membersArray = members as IUser[] ?? members.ToArray();
|
|||
|
if (membersArray.Length == 0)
|
|||
|
{
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
var usr = membersArray[new EllieRandom().Next(0, membersArray.Length)];
|
|||
|
await Response()
|
|||
|
.Confirm("🎟 " + GetText(strs.raffled_user),
|
|||
|
$"**{usr.Username}**",
|
|||
|
footer: $"ID: {usr.Id}")
|
|||
|
.SendAsync();
|
|||
|
}
|
|||
|
|
|||
|
[Cmd]
|
|||
|
[Priority(2)]
|
|||
|
public Task CurrencyTransactions(int page = 1)
|
|||
|
=> InternalCurrencyTransactions(ctx.User.Id, page);
|
|||
|
|
|||
|
[Cmd]
|
|||
|
[OwnerOnly]
|
|||
|
[Priority(0)]
|
|||
|
public Task CurrencyTransactions([Leftover] IUser usr)
|
|||
|
=> InternalCurrencyTransactions(usr.Id, 1);
|
|||
|
|
|||
|
[Cmd]
|
|||
|
[OwnerOnly]
|
|||
|
[Priority(1)]
|
|||
|
public Task CurrencyTransactions(IUser usr, int page)
|
|||
|
=> InternalCurrencyTransactions(usr.Id, page);
|
|||
|
|
|||
|
private async Task InternalCurrencyTransactions(ulong userId, int page)
|
|||
|
{
|
|||
|
if (--page < 0)
|
|||
|
{
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
List<CurrencyTransaction> trs;
|
|||
|
await using (var uow = _db.GetDbContext())
|
|||
|
{
|
|||
|
trs = await uow.Set<CurrencyTransaction>().GetPageFor(userId, page);
|
|||
|
}
|
|||
|
|
|||
|
var embed = _sender.CreateEmbed()
|
|||
|
.WithTitle(GetText(strs.transactions(((SocketGuild)ctx.Guild)?.GetUser(userId)?.ToString()
|
|||
|
?? $"{userId}")))
|
|||
|
.WithOkColor();
|
|||
|
|
|||
|
var sb = new StringBuilder();
|
|||
|
foreach (var tr in trs)
|
|||
|
{
|
|||
|
var change = tr.Amount >= 0 ? "🔵" : "🔴";
|
|||
|
var kwumId = new kwum(tr.Id).ToString();
|
|||
|
var date = $"#{Format.Code(kwumId)} `〖{GetFormattedCurtrDate(tr)}〗`";
|
|||
|
|
|||
|
sb.AppendLine($"\\{change} {date} {Format.Bold(N(tr.Amount))}");
|
|||
|
var transactionString = GetHumanReadableTransaction(tr.Type, tr.Extra, tr.OtherId);
|
|||
|
if (transactionString is not null)
|
|||
|
{
|
|||
|
sb.AppendLine(transactionString);
|
|||
|
}
|
|||
|
|
|||
|
if (!string.IsNullOrWhiteSpace(tr.Note))
|
|||
|
{
|
|||
|
sb.AppendLine($"\t`Note:` {tr.Note.TrimTo(50)}");
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
embed.WithDescription(sb.ToString());
|
|||
|
embed.WithFooter(GetText(strs.page(page + 1)));
|
|||
|
await Response().Embed(embed).SendAsync();
|
|||
|
}
|
|||
|
|
|||
|
private static string GetFormattedCurtrDate(CurrencyTransaction ct)
|
|||
|
=> $"{ct.DateAdded:HH:mm yyyy-MM-dd}";
|
|||
|
|
|||
|
[Cmd]
|
|||
|
public async Task CurrencyTransaction(kwum id)
|
|||
|
{
|
|||
|
int intId = id;
|
|||
|
await using var uow = _db.GetDbContext();
|
|||
|
|
|||
|
var tr = await uow.Set<CurrencyTransaction>()
|
|||
|
.ToLinqToDBTable()
|
|||
|
.Where(x => x.Id == intId && x.UserId == ctx.User.Id)
|
|||
|
.FirstOrDefaultAsync();
|
|||
|
|
|||
|
if (tr is null)
|
|||
|
{
|
|||
|
await Response().Error(strs.not_found).SendAsync();
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
var eb = _sender.CreateEmbed().WithOkColor();
|
|||
|
|
|||
|
eb.WithAuthor(ctx.User);
|
|||
|
eb.WithTitle(GetText(strs.transaction));
|
|||
|
eb.WithDescription(new kwum(tr.Id).ToString());
|
|||
|
eb.AddField("Amount", N(tr.Amount));
|
|||
|
eb.AddField("Type", tr.Type, true);
|
|||
|
eb.AddField("Extra", tr.Extra, true);
|
|||
|
|
|||
|
if (tr.OtherId is ulong other)
|
|||
|
{
|
|||
|
eb.AddField("From Id", other);
|
|||
|
}
|
|||
|
|
|||
|
if (!string.IsNullOrWhiteSpace(tr.Note))
|
|||
|
{
|
|||
|
eb.AddField("Note", tr.Note);
|
|||
|
}
|
|||
|
|
|||
|
eb.WithFooter(GetFormattedCurtrDate(tr));
|
|||
|
|
|||
|
await Response().Embed(eb).SendAsync();
|
|||
|
}
|
|||
|
|
|||
|
private string GetHumanReadableTransaction(string type, string subType, ulong? maybeUserId)
|
|||
|
=> (type, subType, maybeUserId) switch
|
|||
|
{
|
|||
|
("gift", var name, ulong userId) => GetText(strs.curtr_gift(name, userId)),
|
|||
|
("award", var name, ulong userId) => GetText(strs.curtr_award(name, userId)),
|
|||
|
("take", var name, ulong userId) => GetText(strs.curtr_take(name, userId)),
|
|||
|
("blackjack", _, _) => $"Blackjack - {subType}",
|
|||
|
("wheel", _, _) => $"Lucky Ladder - {subType}",
|
|||
|
("lula", _, _) => $"Lucky Ladder - {subType}",
|
|||
|
("rps", _, _) => $"Rock Paper Scissors - {subType}",
|
|||
|
(null, _, _) => null,
|
|||
|
(_, null, _) => null,
|
|||
|
(_, _, ulong userId) => $"{type} - {subType} | [{userId}]",
|
|||
|
_ => $"{type} - {subType}"
|
|||
|
};
|
|||
|
|
|||
|
[Cmd]
|
|||
|
[Priority(0)]
|
|||
|
public async Task Cash(ulong userId)
|
|||
|
{
|
|||
|
var cur = await GetBalanceStringAsync(userId);
|
|||
|
await Response().Confirm(strs.has(Format.Code(userId.ToString()), cur)).SendAsync();
|
|||
|
}
|
|||
|
|
|||
|
private async Task BankAction(SocketMessageComponent smc)
|
|||
|
{
|
|||
|
var balance = await _bank.GetBalanceAsync(ctx.User.Id);
|
|||
|
|
|||
|
await N(balance)
|
|||
|
.Pipe(strs.bank_balance)
|
|||
|
.Pipe(GetText)
|
|||
|
.Pipe(text => smc.RespondConfirmAsync(_sender, text, ephemeral: true));
|
|||
|
}
|
|||
|
|
|||
|
private EllieInteractionBase CreateCashInteraction()
|
|||
|
=> _inter.Create(ctx.User.Id,
|
|||
|
new ButtonBuilder(
|
|||
|
customId: "cash:bank_show_balance",
|
|||
|
emote: new Emoji("🏦")),
|
|||
|
BankAction);
|
|||
|
|
|||
|
[Cmd]
|
|||
|
[Priority(1)]
|
|||
|
public async Task Cash([Leftover] IUser user = null)
|
|||
|
{
|
|||
|
user ??= ctx.User;
|
|||
|
var cur = await GetBalanceStringAsync(user.Id);
|
|||
|
|
|||
|
var inter = user == ctx.User
|
|||
|
? CreateCashInteraction()
|
|||
|
: null;
|
|||
|
|
|||
|
await Response()
|
|||
|
.Confirm(
|
|||
|
user.ToString()
|
|||
|
.Pipe(Format.Bold)
|
|||
|
.With(cur)
|
|||
|
.Pipe(strs.has))
|
|||
|
.Interaction(inter)
|
|||
|
.SendAsync();
|
|||
|
}
|
|||
|
|
|||
|
[Cmd]
|
|||
|
[RequireContext(ContextType.Guild)]
|
|||
|
[Priority(0)]
|
|||
|
public async Task Give(
|
|||
|
[OverrideTypeReader(typeof(BalanceTypeReader))]
|
|||
|
long amount,
|
|||
|
IGuildUser receiver,
|
|||
|
[Leftover] string msg)
|
|||
|
{
|
|||
|
if (amount <= 0 || ctx.User.Id == receiver.Id || receiver.IsBot)
|
|||
|
{
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
if (!await _cs.TransferAsync(_sender, ctx.User, receiver, amount, msg, N(amount)))
|
|||
|
{
|
|||
|
await Response().Error(strs.not_enough(CurrencySign)).SendAsync();
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
await Response().Confirm(strs.gifted(N(amount), Format.Bold(receiver.ToString()), ctx.User)).SendAsync();
|
|||
|
}
|
|||
|
|
|||
|
[Cmd]
|
|||
|
[RequireContext(ContextType.Guild)]
|
|||
|
[Priority(1)]
|
|||
|
public Task Give([OverrideTypeReader(typeof(BalanceTypeReader))] long amount, [Leftover] IGuildUser receiver)
|
|||
|
=> Give(amount, receiver, null);
|
|||
|
|
|||
|
[Cmd]
|
|||
|
[RequireContext(ContextType.Guild)]
|
|||
|
[OwnerOnly]
|
|||
|
[Priority(0)]
|
|||
|
public Task Award(long amount, IGuildUser usr, [Leftover] string msg)
|
|||
|
=> Award(amount, usr.Id, msg);
|
|||
|
|
|||
|
[Cmd]
|
|||
|
[RequireContext(ContextType.Guild)]
|
|||
|
[OwnerOnly]
|
|||
|
[Priority(1)]
|
|||
|
public Task Award(long amount, [Leftover] IGuildUser usr)
|
|||
|
=> Award(amount, usr.Id);
|
|||
|
|
|||
|
[Cmd]
|
|||
|
[OwnerOnly]
|
|||
|
[Priority(2)]
|
|||
|
public async Task Award(long amount, ulong usrId, [Leftover] string msg = null)
|
|||
|
{
|
|||
|
if (amount <= 0)
|
|||
|
{
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
var usr = await ((DiscordSocketClient)Context.Client).Rest.GetUserAsync(usrId);
|
|||
|
|
|||
|
if (usr is null)
|
|||
|
{
|
|||
|
await Response().Error(strs.user_not_found).SendAsync();
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
await _cs.AddAsync(usr.Id, amount, new("award", ctx.User.ToString()!, msg, ctx.User.Id));
|
|||
|
await Response().Confirm(strs.awarded(N(amount), $"<@{usrId}>", ctx.User)).SendAsync();
|
|||
|
}
|
|||
|
|
|||
|
[Cmd]
|
|||
|
[RequireContext(ContextType.Guild)]
|
|||
|
[OwnerOnly]
|
|||
|
[Priority(3)]
|
|||
|
public async Task Award(long amount, [Leftover] IRole role)
|
|||
|
{
|
|||
|
var users = (await ctx.Guild.GetUsersAsync()).Where(u => u.GetRoles().Contains(role)).ToList();
|
|||
|
|
|||
|
await _cs.AddBulkAsync(users.Select(x => x.Id).ToList(),
|
|||
|
amount,
|
|||
|
new("award", ctx.User.ToString()!, role.Name, ctx.User.Id));
|
|||
|
|
|||
|
await Response()
|
|||
|
.Confirm(strs.mass_award(N(amount),
|
|||
|
Format.Bold(users.Count.ToString()),
|
|||
|
Format.Bold(role.Name)))
|
|||
|
.SendAsync();
|
|||
|
}
|
|||
|
|
|||
|
[Cmd]
|
|||
|
[RequireContext(ContextType.Guild)]
|
|||
|
[OwnerOnly]
|
|||
|
[Priority(0)]
|
|||
|
public async Task Take(long amount, [Leftover] IRole role)
|
|||
|
{
|
|||
|
var users = (await role.GetMembersAsync()).ToList();
|
|||
|
|
|||
|
await _cs.RemoveBulkAsync(users.Select(x => x.Id).ToList(),
|
|||
|
amount,
|
|||
|
new("take", ctx.User.ToString()!, null, ctx.User.Id));
|
|||
|
|
|||
|
await Response()
|
|||
|
.Confirm(strs.mass_take(N(amount),
|
|||
|
Format.Bold(users.Count.ToString()),
|
|||
|
Format.Bold(role.Name)))
|
|||
|
.SendAsync();
|
|||
|
}
|
|||
|
|
|||
|
[Cmd]
|
|||
|
[RequireContext(ContextType.Guild)]
|
|||
|
[OwnerOnly]
|
|||
|
[Priority(1)]
|
|||
|
public async Task Take(long amount, [Leftover] IGuildUser user)
|
|||
|
{
|
|||
|
if (amount <= 0)
|
|||
|
{
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
var extra = new TxData("take", ctx.User.ToString()!, null, ctx.User.Id);
|
|||
|
|
|||
|
if (await _cs.RemoveAsync(user.Id, amount, extra))
|
|||
|
{
|
|||
|
await Response().Confirm(strs.take(N(amount), Format.Bold(user.ToString()))).SendAsync();
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
await Response().Error(strs.take_fail(N(amount), Format.Bold(user.ToString()), CurrencySign)).SendAsync();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
[Cmd]
|
|||
|
[OwnerOnly]
|
|||
|
public async Task Take(long amount, [Leftover] ulong usrId)
|
|||
|
{
|
|||
|
if (amount <= 0)
|
|||
|
{
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
var extra = new TxData("take", ctx.User.ToString()!, null, ctx.User.Id);
|
|||
|
|
|||
|
if (await _cs.RemoveAsync(usrId, amount, extra))
|
|||
|
{
|
|||
|
await Response().Confirm(strs.take(N(amount), $"<@{usrId}>")).SendAsync();
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
await Response().Error(strs.take_fail(N(amount), Format.Code(usrId.ToString()), CurrencySign)).SendAsync();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
[Cmd]
|
|||
|
[RequireContext(ContextType.Guild)]
|
|||
|
public async Task RollDuel(IUser u)
|
|||
|
{
|
|||
|
if (ctx.User.Id == u.Id)
|
|||
|
{
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
//since the challenge is created by another user, we need to reverse the ids
|
|||
|
//if it gets removed, means challenge is accepted
|
|||
|
if (_service.Duels.TryRemove((ctx.User.Id, u.Id), out var game))
|
|||
|
{
|
|||
|
await game.StartGame();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
[Cmd]
|
|||
|
[RequireContext(ContextType.Guild)]
|
|||
|
public async Task RollDuel([OverrideTypeReader(typeof(BalanceTypeReader))] long amount, IUser u)
|
|||
|
{
|
|||
|
if (ctx.User.Id == u.Id)
|
|||
|
{
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
if (amount <= 0)
|
|||
|
{
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
var embed = _sender.CreateEmbed().WithOkColor().WithTitle(GetText(strs.roll_duel));
|
|||
|
|
|||
|
var description = string.Empty;
|
|||
|
|
|||
|
var game = new RollDuelGame(_cs, _client.CurrentUser.Id, ctx.User.Id, u.Id, amount);
|
|||
|
//means challenge is just created
|
|||
|
if (_service.Duels.TryGetValue((ctx.User.Id, u.Id), out var other))
|
|||
|
{
|
|||
|
if (other.Amount != amount)
|
|||
|
{
|
|||
|
await Response().Error(strs.roll_duel_already_challenged).SendAsync();
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
await RollDuel(u);
|
|||
|
}
|
|||
|
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
if (_service.Duels.TryAdd((u.Id, ctx.User.Id), game))
|
|||
|
{
|
|||
|
game.OnGameTick += GameOnGameTick;
|
|||
|
game.OnEnded += GameOnEnded;
|
|||
|
|
|||
|
await Response()
|
|||
|
.Confirm(strs.roll_duel_challenge(Format.Bold(ctx.User.ToString()),
|
|||
|
Format.Bold(u.ToString()),
|
|||
|
Format.Bold(N(amount))))
|
|||
|
.SendAsync();
|
|||
|
}
|
|||
|
|
|||
|
async Task GameOnGameTick(RollDuelGame arg)
|
|||
|
{
|
|||
|
var rolls = arg.Rolls.Last();
|
|||
|
description += $@"{Format.Bold(ctx.User.ToString())} rolled **{rolls.Item1}**
|
|||
|
{Format.Bold(u.ToString())} rolled **{rolls.Item2}**
|
|||
|
--
|
|||
|
";
|
|||
|
embed = embed.WithDescription(description);
|
|||
|
|
|||
|
if (rdMsg is null)
|
|||
|
{
|
|||
|
rdMsg = await Response().Embed(embed).SendAsync();
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
await rdMsg.ModifyAsync(x => { x.Embed = embed.Build(); });
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
async Task GameOnEnded(RollDuelGame rdGame, RollDuelGame.Reason reason)
|
|||
|
{
|
|||
|
try
|
|||
|
{
|
|||
|
if (reason == RollDuelGame.Reason.Normal)
|
|||
|
{
|
|||
|
var winner = rdGame.Winner == rdGame.P1 ? ctx.User : u;
|
|||
|
description += $"\n**{winner}** Won {N((long)(rdGame.Amount * 2 * 0.98))}";
|
|||
|
|
|||
|
embed = embed.WithDescription(description);
|
|||
|
|
|||
|
await rdMsg.ModifyAsync(x => x.Embed = embed.Build());
|
|||
|
}
|
|||
|
else if (reason == RollDuelGame.Reason.Timeout)
|
|||
|
{
|
|||
|
await Response().Error(strs.roll_duel_timeout).SendAsync();
|
|||
|
}
|
|||
|
else if (reason == RollDuelGame.Reason.NoFunds)
|
|||
|
{
|
|||
|
await Response().Error(strs.roll_duel_no_funds).SendAsync();
|
|||
|
}
|
|||
|
}
|
|||
|
finally
|
|||
|
{
|
|||
|
_service.Duels.TryRemove((u.Id, ctx.User.Id), out _);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
[Cmd]
|
|||
|
public async Task BetRoll([OverrideTypeReader(typeof(BalanceTypeReader))] long amount)
|
|||
|
{
|
|||
|
if (!await CheckBetMandatory(amount))
|
|||
|
{
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
var maybeResult = await _gs.BetRollAsync(ctx.User.Id, amount);
|
|||
|
if (!maybeResult.TryPickT0(out var result, out _))
|
|||
|
{
|
|||
|
await Response().Error(strs.not_enough(CurrencySign)).SendAsync();
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
var win = (long)result.Won;
|
|||
|
string str;
|
|||
|
if (win > 0)
|
|||
|
{
|
|||
|
str = GetText(strs.br_win(N(win), result.Threshold + (result.Roll == 100 ? " 👑" : "")));
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
str = GetText(strs.better_luck);
|
|||
|
}
|
|||
|
|
|||
|
var eb = _sender.CreateEmbed()
|
|||
|
.WithAuthor(ctx.User)
|
|||
|
.WithDescription(Format.Bold(str))
|
|||
|
.AddField(GetText(strs.roll2), result.Roll.ToString(CultureInfo.InvariantCulture))
|
|||
|
.WithOkColor();
|
|||
|
|
|||
|
await Response().Embed(eb).SendAsync();
|
|||
|
}
|
|||
|
|
|||
|
[Cmd]
|
|||
|
[EllieOptions<LbOpts>]
|
|||
|
[Priority(0)]
|
|||
|
public Task Leaderboard(params string[] args)
|
|||
|
=> Leaderboard(1, args);
|
|||
|
|
|||
|
[Cmd]
|
|||
|
[EllieOptions<LbOpts>]
|
|||
|
[Priority(1)]
|
|||
|
public async Task Leaderboard(int page = 1, params string[] args)
|
|||
|
{
|
|||
|
if (--page < 0)
|
|||
|
{
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
var (opts, _) = OptionsParser.ParseFrom(new LbOpts(), args);
|
|||
|
|
|||
|
// List<DiscordUser> cleanRichest;
|
|||
|
// it's pointless to have clean on dm context
|
|||
|
if (ctx.Guild is null)
|
|||
|
{
|
|||
|
opts.Clean = false;
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
async Task<IReadOnlyCollection<DiscordUser>> GetTopRichest(int curPage)
|
|||
|
{
|
|||
|
if (opts.Clean)
|
|||
|
{
|
|||
|
await ctx.Channel.TriggerTypingAsync();
|
|||
|
await _tracker.EnsureUsersDownloadedAsync(ctx.Guild);
|
|||
|
|
|||
|
await using var uow = _db.GetDbContext();
|
|||
|
|
|||
|
var cleanRichest = await uow.Set<DiscordUser>()
|
|||
|
.GetTopRichest(_client.CurrentUser.Id, 0, 1000);
|
|||
|
|
|||
|
var sg = (SocketGuild)ctx.Guild!;
|
|||
|
return cleanRichest.Where(x => sg.GetUser(x.UserId) is not null).ToList();
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
await using var uow = _db.GetDbContext();
|
|||
|
return await uow.Set<DiscordUser>().GetTopRichest(_client.CurrentUser.Id, curPage);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
var res = Response()
|
|||
|
.Paginated();
|
|||
|
|
|||
|
await Response()
|
|||
|
.Paginated()
|
|||
|
.PageItems(GetTopRichest)
|
|||
|
.TotalElements(900)
|
|||
|
.PageSize(9)
|
|||
|
.CurrentPage(page)
|
|||
|
.Page((toSend, curPage) =>
|
|||
|
{
|
|||
|
var embed = _sender.CreateEmbed()
|
|||
|
.WithOkColor()
|
|||
|
.WithTitle(CurrencySign + " " + GetText(strs.leaderboard));
|
|||
|
|
|||
|
if (!toSend.Any())
|
|||
|
{
|
|||
|
embed.WithDescription(GetText(strs.no_user_on_this_page));
|
|||
|
return Task.FromResult(embed);
|
|||
|
}
|
|||
|
|
|||
|
for (var i = 0; i < toSend.Count; i++)
|
|||
|
{
|
|||
|
var x = toSend[i];
|
|||
|
var usrStr = x.ToString().TrimTo(20, true);
|
|||
|
|
|||
|
var j = i;
|
|||
|
embed.AddField("#" + ((9 * curPage) + j + 1) + " " + usrStr, N(x.CurrencyAmount), true);
|
|||
|
}
|
|||
|
|
|||
|
return Task.FromResult(embed);
|
|||
|
})
|
|||
|
.SendAsync();
|
|||
|
}
|
|||
|
|
|||
|
public enum InputRpsPick : byte
|
|||
|
{
|
|||
|
R = 0,
|
|||
|
Rock = 0,
|
|||
|
Rocket = 0,
|
|||
|
P = 1,
|
|||
|
Paper = 1,
|
|||
|
Paperclip = 1,
|
|||
|
S = 2,
|
|||
|
Scissors = 2
|
|||
|
}
|
|||
|
|
|||
|
[Cmd]
|
|||
|
public async Task Rps(InputRpsPick pick, [OverrideTypeReader(typeof(BalanceTypeReader))] long amount = default)
|
|||
|
{
|
|||
|
static string GetRpsPick(InputRpsPick p)
|
|||
|
{
|
|||
|
switch (p)
|
|||
|
{
|
|||
|
case InputRpsPick.R:
|
|||
|
return "🚀";
|
|||
|
case InputRpsPick.P:
|
|||
|
return "📎";
|
|||
|
default:
|
|||
|
return "✂️";
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
if (!await CheckBetOptional(amount) || amount == 1)
|
|||
|
return;
|
|||
|
|
|||
|
var res = await _gs.RpsAsync(ctx.User.Id, amount, (byte)pick);
|
|||
|
|
|||
|
if (!res.TryPickT0(out var result, out _))
|
|||
|
{
|
|||
|
await Response().Error(strs.not_enough(CurrencySign)).SendAsync();
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
var embed = _sender.CreateEmbed();
|
|||
|
|
|||
|
string msg;
|
|||
|
if (result.Result == RpsResultType.Draw)
|
|||
|
{
|
|||
|
msg = GetText(strs.rps_draw(GetRpsPick(pick)));
|
|||
|
}
|
|||
|
else if (result.Result == RpsResultType.Win)
|
|||
|
{
|
|||
|
if ((long)result.Won > 0)
|
|||
|
embed.AddField(GetText(strs.won), N((long)result.Won));
|
|||
|
|
|||
|
msg = GetText(strs.rps_win(ctx.User.Mention,
|
|||
|
GetRpsPick(pick),
|
|||
|
GetRpsPick((InputRpsPick)result.ComputerPick)));
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
msg = GetText(strs.rps_win(ctx.Client.CurrentUser.Mention,
|
|||
|
GetRpsPick((InputRpsPick)result.ComputerPick),
|
|||
|
GetRpsPick(pick)));
|
|||
|
}
|
|||
|
|
|||
|
embed
|
|||
|
.WithOkColor()
|
|||
|
.WithDescription(msg);
|
|||
|
|
|||
|
await Response().Embed(embed).SendAsync();
|
|||
|
}
|
|||
|
|
|||
|
private static readonly ImmutableArray<string> _emojis =
|
|||
|
new[] { "⬆", "↖", "⬅", "↙", "⬇", "↘", "➡", "↗" }.ToImmutableArray();
|
|||
|
|
|||
|
[Cmd]
|
|||
|
public async Task LuckyLadder([OverrideTypeReader(typeof(BalanceTypeReader))] long amount)
|
|||
|
{
|
|||
|
if (!await CheckBetMandatory(amount))
|
|||
|
return;
|
|||
|
|
|||
|
var res = await _gs.LulaAsync(ctx.User.Id, amount);
|
|||
|
if (!res.TryPickT0(out var result, out _))
|
|||
|
{
|
|||
|
await Response().Error(strs.not_enough(CurrencySign)).SendAsync();
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
var multis = result.Multipliers;
|
|||
|
|
|||
|
var sb = new StringBuilder();
|
|||
|
foreach (var multi in multis)
|
|||
|
{
|
|||
|
sb.Append($"╠══╣");
|
|||
|
|
|||
|
if (multi == result.Multiplier)
|
|||
|
sb.Append($"{Format.Bold($"x{multi:0.##}")} ⬅️");
|
|||
|
else
|
|||
|
sb.Append($"||x{multi:0.##}||");
|
|||
|
|
|||
|
sb.AppendLine();
|
|||
|
}
|
|||
|
|
|||
|
var eb = _sender.CreateEmbed()
|
|||
|
.WithOkColor()
|
|||
|
.WithDescription(sb.ToString())
|
|||
|
.AddField(GetText(strs.multiplier), $"{result.Multiplier:0.##}x", true)
|
|||
|
.AddField(GetText(strs.won), $"{(long)result.Won}", true)
|
|||
|
.WithAuthor(ctx.User);
|
|||
|
|
|||
|
|
|||
|
await Response().Embed(eb).SendAsync();
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
public enum GambleTestTarget
|
|||
|
{
|
|||
|
Slot,
|
|||
|
Betroll,
|
|||
|
Betflip,
|
|||
|
BetflipT,
|
|||
|
BetDraw,
|
|||
|
BetDrawHL,
|
|||
|
BetDrawRB,
|
|||
|
Lula,
|
|||
|
Rps,
|
|||
|
}
|
|||
|
|
|||
|
[Cmd]
|
|||
|
[OwnerOnly]
|
|||
|
public async Task BetTest()
|
|||
|
{
|
|||
|
var values = Enum.GetValues<GambleTestTarget>()
|
|||
|
.Select(x => $"`{x}`")
|
|||
|
.Join(", ");
|
|||
|
|
|||
|
await Response().Confirm(GetText(strs.available_tests), values).SendAsync();
|
|||
|
}
|
|||
|
|
|||
|
[Cmd]
|
|||
|
[OwnerOnly]
|
|||
|
public async Task BetTest(GambleTestTarget target, int tests = 1000)
|
|||
|
{
|
|||
|
if (tests <= 0)
|
|||
|
return;
|
|||
|
|
|||
|
await ctx.Channel.TriggerTypingAsync();
|
|||
|
|
|||
|
var streak = 0;
|
|||
|
var maxW = 0;
|
|||
|
var maxL = 0;
|
|||
|
|
|||
|
var dict = new Dictionary<decimal, int>();
|
|||
|
for (var i = 0; i < tests; i++)
|
|||
|
{
|
|||
|
var multi = target switch
|
|||
|
{
|
|||
|
GambleTestTarget.BetDraw => (await _gs.BetDrawAsync(ctx.User.Id, 0, 1, 0)).AsT0.Multiplier,
|
|||
|
GambleTestTarget.BetDrawRB => (await _gs.BetDrawAsync(ctx.User.Id, 0, null, 1)).AsT0.Multiplier,
|
|||
|
GambleTestTarget.BetDrawHL => (await _gs.BetDrawAsync(ctx.User.Id, 0, 0, null)).AsT0.Multiplier,
|
|||
|
GambleTestTarget.Slot => (await _gs.SlotAsync(ctx.User.Id, 0)).AsT0.Multiplier,
|
|||
|
GambleTestTarget.Betflip => (await _gs.BetFlipAsync(ctx.User.Id, 0, 0)).AsT0.Multiplier,
|
|||
|
GambleTestTarget.BetflipT => (await _gs.BetFlipAsync(ctx.User.Id, 0, 1)).AsT0.Multiplier,
|
|||
|
GambleTestTarget.Lula => (await _gs.LulaAsync(ctx.User.Id, 0)).AsT0.Multiplier,
|
|||
|
GambleTestTarget.Rps => (await _gs.RpsAsync(ctx.User.Id, 0, (byte)(i % 3))).AsT0.Multiplier,
|
|||
|
GambleTestTarget.Betroll => (await _gs.BetRollAsync(ctx.User.Id, 0)).AsT0.Multiplier,
|
|||
|
_ => throw new ArgumentOutOfRangeException(nameof(target))
|
|||
|
};
|
|||
|
|
|||
|
if (dict.ContainsKey(multi))
|
|||
|
dict[multi] += 1;
|
|||
|
else
|
|||
|
dict.Add(multi, 1);
|
|||
|
|
|||
|
if (multi < 1)
|
|||
|
{
|
|||
|
if (streak <= 0)
|
|||
|
--streak;
|
|||
|
else
|
|||
|
streak = -1;
|
|||
|
|
|||
|
maxL = Math.Max(maxL, -streak);
|
|||
|
}
|
|||
|
else if (multi > 1)
|
|||
|
{
|
|||
|
if (streak >= 0)
|
|||
|
++streak;
|
|||
|
else
|
|||
|
streak = 1;
|
|||
|
|
|||
|
maxW = Math.Max(maxW, streak);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
var sb = new StringBuilder();
|
|||
|
decimal payout = 0;
|
|||
|
foreach (var key in dict.Keys.OrderByDescending(x => x))
|
|||
|
{
|
|||
|
sb.AppendLine($"x**{key}** occured `{dict[key]}` times. {dict[key] * 1.0f / tests * 100}%");
|
|||
|
payout += key * dict[key];
|
|||
|
}
|
|||
|
|
|||
|
sb.AppendLine();
|
|||
|
sb.AppendLine($"Longest win streak: `{maxW}`");
|
|||
|
sb.AppendLine($"Longest lose streak: `{maxL}`");
|
|||
|
|
|||
|
await Response()
|
|||
|
.Confirm(GetText(strs.test_results_for(target)),
|
|||
|
sb.ToString(),
|
|||
|
footer: $"Total Bet: {tests} | Payout: {payout:F0} | {payout * 1.0M / tests * 100}%")
|
|||
|
.SendAsync();
|
|||
|
}
|
|||
|
}
|