diff --git a/src/EllieBot/Migrations/PostgreSql/20250328075848_quests.sql b/src/EllieBot/Migrations/PostgreSql/20250328075848_quests.sql new file mode 100644 index 0000000..4392fe6 --- /dev/null +++ b/src/EllieBot/Migrations/PostgreSql/20250328075848_quests.sql @@ -0,0 +1,6 @@ +START TRANSACTION; +INSERT INTO "__EFMigrationsHistory" (migrationid, productversion) +VALUES ('20250328075848_quests', '9.0.1'); + +COMMIT; + diff --git a/src/EllieBot/Migrations/PostgreSql/20250323022235_init.Designer.cs b/src/EllieBot/Migrations/PostgreSql/20250328080459_init.Designer.cs similarity index 99% rename from src/EllieBot/Migrations/PostgreSql/20250323022235_init.Designer.cs rename to src/EllieBot/Migrations/PostgreSql/20250328080459_init.Designer.cs index 45a075b..fb946c4 100644 --- a/src/EllieBot/Migrations/PostgreSql/20250323022235_init.Designer.cs +++ b/src/EllieBot/Migrations/PostgreSql/20250328080459_init.Designer.cs @@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace EllieBot.Migrations.PostgreSql { [DbContext(typeof(PostgreSqlContext))] - [Migration("20250323022235_init")] + [Migration("20250328080459_init")] partial class init { /// diff --git a/src/EllieBot/Migrations/PostgreSql/20250323022235_init.cs b/src/EllieBot/Migrations/PostgreSql/20250328080459_init.cs similarity index 100% rename from src/EllieBot/Migrations/PostgreSql/20250323022235_init.cs rename to src/EllieBot/Migrations/PostgreSql/20250328080459_init.cs diff --git a/src/EllieBot/Migrations/Sqlite/20250328075818_quests.sql b/src/EllieBot/Migrations/Sqlite/20250328075818_quests.sql new file mode 100644 index 0000000..aa0c9cd --- /dev/null +++ b/src/EllieBot/Migrations/Sqlite/20250328075818_quests.sql @@ -0,0 +1,6 @@ +BEGIN TRANSACTION; +INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") +VALUES ('20250328075818_quests', '9.0.1'); + +COMMIT; + diff --git a/src/EllieBot/Migrations/Sqlite/20250323022218_init.Designer.cs b/src/EllieBot/Migrations/Sqlite/20250328080413_init.Designer.cs similarity index 99% rename from src/EllieBot/Migrations/Sqlite/20250323022218_init.Designer.cs rename to src/EllieBot/Migrations/Sqlite/20250328080413_init.Designer.cs index 799caba..93bcd31 100644 --- a/src/EllieBot/Migrations/Sqlite/20250323022218_init.Designer.cs +++ b/src/EllieBot/Migrations/Sqlite/20250328080413_init.Designer.cs @@ -11,7 +11,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace EllieBot.Migrations.Sqlite { [DbContext(typeof(SqliteContext))] - [Migration("20250323022218_init")] + [Migration("20250328080413_init")] partial class init { /// diff --git a/src/EllieBot/Migrations/Sqlite/20250323022218_init.cs b/src/EllieBot/Migrations/Sqlite/20250328080413_init.cs similarity index 100% rename from src/EllieBot/Migrations/Sqlite/20250323022218_init.cs rename to src/EllieBot/Migrations/Sqlite/20250328080413_init.cs diff --git a/src/EllieBot/Modules/Gambling/AnimalRacing/AnimalRace.cs b/src/EllieBot/Modules/Gambling/AnimalRacing/AnimalRace.cs index 778de60..14cd2cd 100644 --- a/src/EllieBot/Modules/Gambling/AnimalRacing/AnimalRace.cs +++ b/src/EllieBot/Modules/Gambling/AnimalRacing/AnimalRace.cs @@ -1,6 +1,7 @@ #nullable disable using EllieBot.Modules.Gambling.Common.AnimalRacing.Exceptions; using EllieBot.Modules.Games.Common; +using EllieBot.Modules.Games.Quests; namespace EllieBot.Modules.Gambling.Common.AnimalRacing; @@ -35,12 +36,18 @@ public sealed class AnimalRace : IDisposable private readonly ICurrencyService _currency; private readonly RaceOptions _options; private readonly Queue _animalsQueue; + private readonly QuestService _quests; - public AnimalRace(RaceOptions options, ICurrencyService currency, IEnumerable availableAnimals) + public AnimalRace( + RaceOptions options, + ICurrencyService currency, + IEnumerable availableAnimals, + QuestService quests) { _currency = currency; _options = options; _animalsQueue = new(availableAnimals); + _quests = quests; MaxUsers = _animalsQueue.Count; if (_animalsQueue.Count == 0) @@ -60,7 +67,10 @@ public sealed class AnimalRace : IDisposable await Start(); } - finally { _locker.Release(); } + finally + { + _locker.Release(); + } }); public async Task JoinRace(ulong userId, string userName, long bet = 0) @@ -93,7 +103,10 @@ public sealed class AnimalRace : IDisposable return user; } - finally { _locker.Release(); } + finally + { + _locker.Release(); + } } private async Task Start() @@ -104,7 +117,9 @@ public sealed class AnimalRace : IDisposable foreach (var user in _users) { if (user.Bet > 0) - await _currency.AddAsync(user.UserId, (long)(user.Bet + BASE_MULTIPLIER), new("animalrace", "refund")); + await _currency.AddAsync(user.UserId, + (long)(user.Bet * BASE_MULTIPLIER), + new("animalrace", "refund")); } _ = OnStartingFailed?.Invoke(this); @@ -112,6 +127,11 @@ public sealed class AnimalRace : IDisposable return; } + foreach (var user in _users) + { + await _quests.ReportActionAsync(user.UserId, QuestEventType.RaceJoined); + } + _ = OnStarted?.Invoke(this); _ = Task.Run(async () => { diff --git a/src/EllieBot/Modules/Gambling/AnimalRacing/AnimalRacingCommands.cs b/src/EllieBot/Modules/Gambling/AnimalRacing/AnimalRacingCommands.cs index 5a4be4e..31557d1 100644 --- a/src/EllieBot/Modules/Gambling/AnimalRacing/AnimalRacingCommands.cs +++ b/src/EllieBot/Modules/Gambling/AnimalRacing/AnimalRacingCommands.cs @@ -4,6 +4,7 @@ using EllieBot.Modules.Gambling.Common; using EllieBot.Modules.Gambling.Common.AnimalRacing; using EllieBot.Modules.Gambling.Common.AnimalRacing.Exceptions; using EllieBot.Modules.Gambling.Services; +using EllieBot.Modules.Games.Quests; using EllieBot.Modules.Games.Services; namespace EllieBot.Modules.Gambling; @@ -17,6 +18,7 @@ public partial class Gambling private readonly ICurrencyService _cs; private readonly DiscordSocketClient _client; private readonly GamesConfigService _gamesConf; + private readonly QuestService _quests; private IUserMessage raceMessage; @@ -24,12 +26,14 @@ public partial class Gambling ICurrencyService cs, DiscordSocketClient client, GamblingConfigService gamblingConf, - GamesConfigService gamesConf) + GamesConfigService gamesConf, + QuestService quests) : base(gamblingConf) { _cs = cs; _client = client; _gamesConf = gamesConf; + _quests = quests; } [Cmd] @@ -39,11 +43,11 @@ public partial class Gambling { var (options, _) = OptionsParser.ParseFrom(new RaceOptions(), args); - var ar = new AnimalRace(options, _cs, _gamesConf.Data.RaceAnimals.Shuffle()); + var ar = new AnimalRace(options, _cs, _gamesConf.Data.RaceAnimals.Shuffle(), _quests); if (!_service.AnimalRaces.TryAdd(ctx.Guild.Id, ar)) return Response() - .Error(GetText(strs.animal_race), GetText(strs.animal_race_already_started)) - .SendAsync(); + .Error(GetText(strs.animal_race), GetText(strs.animal_race_already_started)) + .SendAsync(); ar.Initialize(); @@ -61,7 +65,9 @@ public partial class Gambling raceMessage = null; } } - catch { } + catch + { + } }); return Task.CompletedTask; } @@ -74,22 +80,22 @@ public partial class Gambling if (race.FinishedUsers[0].Bet > 0) { return Response() - .Embed(CreateEmbed() - .WithOkColor() - .WithTitle(GetText(strs.animal_race)) - .WithDescription(GetText(strs.animal_race_won_money( - Format.Bold(winner.Username), - winner.Animal.Icon, - N(race.FinishedUsers[0].Bet * race.Multi)))) - .WithFooter($"x{race.Multi:F2}")) - .SendAsync(); + .Embed(CreateEmbed() + .WithOkColor() + .WithTitle(GetText(strs.animal_race)) + .WithDescription(GetText(strs.animal_race_won_money( + Format.Bold(winner.Username), + winner.Animal.Icon, + N(race.FinishedUsers[0].Bet * race.Multi)))) + .WithFooter($"x{race.Multi:F2}")) + .SendAsync(); } ar.Dispose(); return Response() - .Confirm(GetText(strs.animal_race), - GetText(strs.animal_race_won(Format.Bold(winner.Username), winner.Animal.Icon))) - .SendAsync(); + .Confirm(GetText(strs.animal_race), + GetText(strs.animal_race_won(Format.Bold(winner.Username), winner.Animal.Icon))) + .SendAsync(); } ar.OnStartingFailed += Ar_OnStartingFailed; @@ -99,10 +105,10 @@ public partial class Gambling _client.MessageReceived += ClientMessageReceived; return Response() - .Confirm(GetText(strs.animal_race), - GetText(strs.animal_race_starting(options.StartTime)), - footer: GetText(strs.animal_race_join_instr(prefix))) - .SendAsync(); + .Confirm(GetText(strs.animal_race), + GetText(strs.animal_race_starting(options.StartTime)), + footer: GetText(strs.animal_race_join_instr(prefix))) + .SendAsync(); } private Task Ar_OnStarted(AnimalRace race) @@ -110,9 +116,9 @@ public partial class Gambling if (race.Users.Count == race.MaxUsers) return Response().Confirm(GetText(strs.animal_race), GetText(strs.animal_race_full)).SendAsync(); return Response() - .Confirm(GetText(strs.animal_race), - GetText(strs.animal_race_starting_with_x(race.Users.Count))) - .SendAsync(); + .Confirm(GetText(strs.animal_race), + GetText(strs.animal_race_starting_with_x(race.Users.Count))) + .SendAsync(); } private async Task Ar_OnStateUpdate(AnimalRace race) @@ -133,10 +139,10 @@ public partial class Gambling else { await msg.ModifyAsync(x => x.Embed = CreateEmbed() - .WithTitle(GetText(strs.animal_race)) - .WithDescription(text) - .WithOkColor() - .Build()); + .WithTitle(GetText(strs.animal_race)) + .WithDescription(text) + .WithOkColor() + .Build()); } } @@ -166,15 +172,15 @@ public partial class Gambling if (amount > 0) { await Response() - .Confirm(GetText(strs.animal_race_join_bet(ctx.User.Mention, - user.Animal.Icon, - amount + CurrencySign))) - .SendAsync(); + .Confirm(GetText(strs.animal_race_join_bet(ctx.User.Mention, + user.Animal.Icon, + amount + CurrencySign))) + .SendAsync(); } else await Response() - .Confirm(strs.animal_race_join(ctx.User.Mention, user.Animal.Icon)) - .SendAsync(); + .Confirm(strs.animal_race_join(ctx.User.Mention, user.Animal.Icon)) + .SendAsync(); } catch (ArgumentOutOfRangeException) { diff --git a/src/EllieBot/Modules/Gambling/Bank/BankService.cs b/src/EllieBot/Modules/Gambling/Bank/BankService.cs index 0d75607..00225fe 100644 --- a/src/EllieBot/Modules/Gambling/Bank/BankService.cs +++ b/src/EllieBot/Modules/Gambling/Bank/BankService.cs @@ -1,20 +1,15 @@ using LinqToDB; using LinqToDB.EntityFrameworkCore; using EllieBot.Db.Models; +using EllieBot.Modules.Games.Quests; namespace EllieBot.Modules.Gambling.Bank; -public sealed class BankService : IBankService, IEService +public sealed class BankService( + ICurrencyService _cur, + DbService _db, + QuestService quests) : IBankService, IEService { - private readonly ICurrencyService _cur; - private readonly DbService _db; - - public BankService(ICurrencyService cur, DbService db) - { - _cur = cur; - _db = db; - } - public async Task AwardAsync(ulong userId, long amount) { ArgumentOutOfRangeException.ThrowIfNegativeOrZero(amount); @@ -37,7 +32,7 @@ public sealed class BankService : IBankService, IEService return true; } - + public async Task TakeAsync(ulong userId, long amount) { ArgumentOutOfRangeException.ThrowIfNegativeOrZero(amount); @@ -50,7 +45,7 @@ public sealed class BankService : IBankService, IEService { Balance = old.Balance - amount }); - + return rows > 0; } @@ -63,20 +58,28 @@ public sealed class BankService : IBankService, IEService await using var ctx = _db.GetDbContext(); await ctx.Set() - .ToLinqToDBTable() - .InsertOrUpdateAsync(() => new() - { - UserId = userId, - Balance = amount - }, - (old) => new() - { - Balance = old.Balance + amount - }, - () => new() - { - UserId = userId - }); + .ToLinqToDBTable() + .InsertOrUpdateAsync(() => new() + { + UserId = userId, + Balance = amount + }, + (old) => new() + { + Balance = old.Balance + amount + }, + () => new() + { + UserId = userId + }); + + await quests.ReportActionAsync(userId, + QuestEventType.BankAction, + new() + { + { "type", "deposit" }, + { "amount", amount.ToString() } + }); return true; } @@ -87,12 +90,12 @@ public sealed class BankService : IBankService, IEService await using var ctx = _db.GetDbContext(); var rows = await ctx.Set() - .ToLinqToDBTable() - .Where(x => x.UserId == userId && x.Balance >= amount) - .UpdateAsync((old) => new() - { - Balance = old.Balance - amount - }); + .ToLinqToDBTable() + .Where(x => x.UserId == userId && x.Balance >= amount) + .UpdateAsync((old) => new() + { + Balance = old.Balance - amount + }); if (rows > 0) { @@ -106,10 +109,11 @@ public sealed class BankService : IBankService, IEService public async Task GetBalanceAsync(ulong userId) { await using var ctx = _db.GetDbContext(); - return (await ctx.Set() - .ToLinqToDBTable() - .FirstOrDefaultAsync(x => x.UserId == userId)) - ?.Balance - ?? 0; + var res = (await ctx.Set() + .ToLinqToDBTable() + .FirstOrDefaultAsync(x => x.UserId == userId)) + ?.Balance + ?? 0; + return res; } } \ No newline at end of file diff --git a/src/EllieBot/Modules/Gambling/Gambling.cs b/src/EllieBot/Modules/Gambling/Gambling.cs index 4059bdb..e6e9392 100644 --- a/src/EllieBot/Modules/Gambling/Gambling.cs +++ b/src/EllieBot/Modules/Gambling/Gambling.cs @@ -14,6 +14,7 @@ using System.Text; using EllieBot.Modules.Gambling.Rps; using EllieBot.Common.TypeReaders; using EllieBot.Modules.Games; +using EllieBot.Modules.Games.Quests; using EllieBot.Modules.Patronage; namespace EllieBot.Modules.Gambling; @@ -36,6 +37,7 @@ public partial class Gambling : GamblingModule private readonly IBotCache _cache; private readonly CaptchaService _captchaService; private readonly VoteRewardService _vrs; + private readonly QuestService _quests; public Gambling( IGamblingService gs, @@ -52,7 +54,8 @@ public partial class Gambling : GamblingModule RakebackService rb, IBotCache cache, CaptchaService captchaService, - VoteRewardService vrs) + VoteRewardService vrs, + QuestService quests) : base(configService) { _gs = gs; @@ -68,6 +71,7 @@ public partial class Gambling : GamblingModule _ps = patronage; _rng = new EllieRandom(); _vrs = vrs; + _quests = quests; _enUsCulture = new CultureInfo("en-US", false).NumberFormat; _enUsCulture.NumberDecimalDigits = 0; diff --git a/src/EllieBot/Modules/Gambling/GamblingService.cs b/src/EllieBot/Modules/Gambling/GamblingService.cs index 96f715a..8b40a99 100644 --- a/src/EllieBot/Modules/Gambling/GamblingService.cs +++ b/src/EllieBot/Modules/Gambling/GamblingService.cs @@ -6,6 +6,7 @@ using EllieBot.Common.ModuleBehaviors; using EllieBot.Db.Models; using EllieBot.Modules.Gambling.Common; using EllieBot.Modules.Gambling.Common.Connect4; +using EllieBot.Modules.Games.Quests; using EllieBot.Modules.Patronage; namespace EllieBot.Modules.Gambling.Services; @@ -19,6 +20,7 @@ public class GamblingService : IEService, IReadyExecutor private readonly IBotCache _cache; private readonly GamblingConfigService _gcs; private readonly IPatronageService _ps; + private readonly QuestService _quests; private readonly EllieRandom _rng; private static readonly TypedKey _curDecayKey = new("currency:last_decay"); @@ -28,13 +30,15 @@ public class GamblingService : IEService, IReadyExecutor DiscordSocketClient client, IBotCache cache, GamblingConfigService gcs, - IPatronageService ps) + IPatronageService ps, + QuestService quests) { _db = db; _client = client; _cache = cache; _gcs = gcs; _ps = ps; + _quests = quests; _rng = new EllieRandom(); } @@ -230,10 +234,15 @@ public class GamblingService : IEService, IReadyExecutor if (booster) originalAmount += gcsData.BoostBonus.BaseTimelyBonus; + var hasCompletedDailies = await _quests.UserCompletedDailies(userId); + + if (hasCompletedDailies) + originalAmount = (long)(1.5 * originalAmount); + var patron = await _ps.GetPatronAsync(userId); var percentBonus = (_ps.PercentBonus(patron) / 100f); - originalAmount += (int)(originalAmount * percentBonus); + originalAmount += (long)(originalAmount * percentBonus); var msg = $"**{N(originalAmount)}** base reward\n\n"; if (boostGuilds.Count > 0) @@ -252,6 +261,15 @@ public class GamblingService : IEService, IReadyExecutor else msg += $"\\❌ *+0 bonus for the [Patreon](https://patreon.com/elliebot) pledge*\n"; } + + if (hasCompletedDailies) + { + msg += $"\\✅ *+50% bonus for completing daily quests*\n"; + } + else + { + msg += $"\\❌ *+0 bonus for completing daily quests*\n"; + } return (originalAmount, msg); } diff --git a/src/EllieBot/Modules/Gambling/PlantPick/PlantPickService.cs b/src/EllieBot/Modules/Gambling/PlantPick/PlantPickService.cs index 8ade01d..2b92219 100644 --- a/src/EllieBot/Modules/Gambling/PlantPick/PlantPickService.cs +++ b/src/EllieBot/Modules/Gambling/PlantPick/PlantPickService.cs @@ -4,6 +4,7 @@ using LinqToDB.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using EllieBot.Common.ModuleBehaviors; using EllieBot.Db.Models; +using EllieBot.Modules.Games.Quests; using SixLabors.Fonts; using SixLabors.Fonts.Unicode; using SixLabors.ImageSharp; @@ -15,67 +16,47 @@ using Image = SixLabors.ImageSharp.Image; namespace EllieBot.Modules.Gambling.Services; -public class PlantPickService : IEService, IExecNoCommand, IReadyExecutor +public class PlantPickService( + DbService db, + IBotStrings strings, + IImageCache images, + FontProvider fonts, + ICurrencyService cs, + CommandHandler cmdHandler, + DiscordSocketClient client, + GamblingConfigService gss, + GamblingService gs, + QuestService quests) : IEService, IExecNoCommand, IReadyExecutor { //channelId/last generation public ConcurrentDictionary LastGenerations { get; } = new(); - private readonly DbService _db; - private readonly IBotStrings _strings; - private readonly IImageCache _images; - private readonly FontProvider _fonts; - private readonly ICurrencyService _cs; - private readonly CommandHandler _cmdHandler; - private readonly DiscordSocketClient _client; - private readonly GamblingConfigService _gss; - private readonly GamblingService _gs; - private ConcurrentHashSet _generationChannels = []; - public PlantPickService( - DbService db, - IBotStrings strings, - IImageCache images, - FontProvider fonts, - ICurrencyService cs, - CommandHandler cmdHandler, - DiscordSocketClient client, - GamblingConfigService gss, - GamblingService gs) - { - _db = db; - _strings = strings; - _images = images; - _fonts = fonts; - _cs = cs; - _cmdHandler = cmdHandler; - _client = client; - _gss = gss; - _gs = gs; - } - public Task ExecOnNoCommandAsync(IGuild guild, IUserMessage msg) => PotentialFlowerGeneration(msg); private string GetText(ulong gid, LocStr str) - => _strings.GetText(str, gid); + => strings.GetText(str, gid); public async Task ToggleCurrencyGeneration(ulong gid, ulong cid) { bool enabled; - await using var uow = _db.GetDbContext(); + await using var uow = db.GetDbContext(); if (_generationChannels.Add(cid)) { await uow.GetTable() - .InsertOrUpdateAsync(() => new() + .InsertOrUpdateAsync(() => new() { ChannelId = cid, GuildId = gid - }, (x) => new() + }, + (x) => new() { ChannelId = cid, GuildId = gid - }, () => new() + }, + () => new() { ChannelId = cid, GuildId = gid @@ -87,8 +68,8 @@ public class PlantPickService : IEService, IExecNoCommand, IReadyExecutor else { await uow.GetTable() - .Where(x => x.ChannelId == cid && x.GuildId == gid) - .DeleteAsync(); + .Where(x => x.ChannelId == cid && x.GuildId == gid) + .DeleteAsync(); _generationChannels.TryRemove(cid); enabled = false; @@ -99,9 +80,9 @@ public class PlantPickService : IEService, IExecNoCommand, IReadyExecutor public async Task> GetAllGeneratingChannels() { - await using var uow = _db.GetDbContext(); + await using var uow = db.GetDbContext(); return await uow.GetTable() - .ToListAsyncLinqToDB(); + .ToListAsyncLinqToDB(); } /// @@ -111,7 +92,7 @@ public class PlantPickService : IEService, IExecNoCommand, IReadyExecutor /// Stream of the currency image public async Task<(Stream, string)> GetRandomCurrencyImageAsync(string pass) { - var curImg = await _images.GetCurrencyImageAsync(); + var curImg = await images.GetCurrencyImageAsync(); if (curImg is null) return (new MemoryStream(), null); @@ -142,7 +123,7 @@ public class PlantPickService : IEService, IExecNoCommand, IReadyExecutor pass = pass.TrimTo(10, true).ToLowerInvariant(); using var img = Image.Load(curImg); // choose font size based on the image height, so that it's visible - var font = _fonts.NotoSans.CreateFont(img.Height / 11.0f, FontStyle.Bold); + var font = fonts.NotoSans.CreateFont(img.Height / 11.0f, FontStyle.Bold); img.Mutate(x => { // measure the size of the text to be drawing @@ -170,13 +151,13 @@ public class PlantPickService : IEService, IExecNoCommand, IReadyExecutor // draw the password over the background x.DrawText(new RichTextOptions(font) - { - Origin = new(0, 0), - TextRuns = + { + Origin = new(0, 0), + TextRuns = [ strikeoutRun ] - }, + }, pass, new SolidBrush(Color.White)); }); @@ -200,7 +181,7 @@ public class PlantPickService : IEService, IExecNoCommand, IReadyExecutor { try { - var config = _gss.Data; + var config = gss.Data; var lastGeneration = LastGenerations.GetOrAdd(channel.Id, DateTime.MinValue.ToBinary()); var rng = new EllieRandom(); @@ -219,7 +200,7 @@ public class PlantPickService : IEService, IExecNoCommand, IReadyExecutor if (dropAmount > 0) { - var prefix = _cmdHandler.GetPrefix(channel.Guild.Id); + var prefix = cmdHandler.GetPrefix(channel.Guild.Id); var toSend = dropAmount == 1 ? GetText(channel.GuildId, strs.curgen_sn(config.Currency.Sign)) + " " @@ -228,7 +209,7 @@ public class PlantPickService : IEService, IExecNoCommand, IReadyExecutor + " " + GetText(channel.GuildId, strs.pick_pl(prefix)); - var pw = config.Generation.HasPassword ? _gs.GeneratePassword().ToUpperInvariant() : null; + var pw = config.Generation.HasPassword ? gs.GeneratePassword().ToUpperInvariant() : null; IUserMessage sent; var (stream, ext) = await GetRandomCurrencyImageAsync(pw); @@ -238,7 +219,7 @@ public class PlantPickService : IEService, IExecNoCommand, IReadyExecutor var res = await AddPlantToDatabase(channel.GuildId, channel.Id, - _client.CurrentUser.Id, + client.CurrentUser.Id, sent.Id, dropAmount, pw, @@ -261,12 +242,12 @@ public class PlantPickService : IEService, IExecNoCommand, IReadyExecutor public async Task PickAsync( ulong gid, ITextChannel ch, - ulong uid, + ulong userId, string pass) { long amount; ulong[] ids; - await using (var uow = _db.GetDbContext()) + await using (var uow = db.GetDbContext()) { // this method will sum all plants with that password, // remove them, and get messageids of the removed plants @@ -274,8 +255,8 @@ public class PlantPickService : IEService, IExecNoCommand, IReadyExecutor pass = pass?.Trim().TrimTo(10, true)?.ToUpperInvariant(); // gets all plants in this channel with the same password var entries = await uow.GetTable() - .Where(x => x.ChannelId == ch.Id && pass == x.Password) - .DeleteWithOutputAsync(); + .Where(x => x.ChannelId == ch.Id && pass == x.Password) + .DeleteWithOutputAsync(); if (!entries.Any()) return 0; @@ -285,14 +266,24 @@ public class PlantPickService : IEService, IExecNoCommand, IReadyExecutor } if (amount > 0) - await _cs.AddAsync(uid, amount, new("currency", "collect")); + { + await cs.AddAsync(userId, amount, new("currency", "collect")); + await quests.ReportActionAsync(userId, + QuestEventType.PlantOrPick, + new() + { + { "type", "pick" }, + }); + } try { _ = ch.DeleteMessagesAsync(ids); } - catch { } + catch + { + } // return the amount of currency the user picked return amount; @@ -308,8 +299,8 @@ public class PlantPickService : IEService, IExecNoCommand, IReadyExecutor try { // get the text - var prefix = _cmdHandler.GetPrefix(gid); - var msgToSend = GetText(gid, strs.planted(Format.Bold(user), amount + _gss.Data.Currency.Sign)); + var prefix = cmdHandler.GetPrefix(gid); + var msgToSend = GetText(gid, strs.planted(Format.Bold(user), amount + gss.Data.Currency.Sign)); if (amount > 1) msgToSend += " " + GetText(gid, strs.pick_pl(prefix)); @@ -337,7 +328,7 @@ public class PlantPickService : IEService, IExecNoCommand, IReadyExecutor public async Task PlantAsync( ulong gid, ITextChannel ch, - ulong uid, + ulong userId, string user, long amount, string pass) @@ -349,19 +340,20 @@ public class PlantPickService : IEService, IExecNoCommand, IReadyExecutor return false; // remove currency from the user who's planting - if (await _cs.RemoveAsync(uid, amount, new("put/collect", "put"))) + if (await cs.RemoveAsync(userId, amount, new("put/collect", "put"))) { // try to send the message with the currency image var msgId = await SendPlantMessageAsync(gid, ch, user, amount, pass); if (msgId is null) { // if it fails it will return null, if it returns null, refund - await _cs.AddAsync(uid, amount, new("put/collect", "refund")); + await cs.AddAsync(userId, amount, new("put/collect", "refund")); return false; } // if it doesn't fail, put the plant in the database for other people to pick - await AddPlantToDatabase(gid, ch.Id, uid, msgId.Value, amount, pass); + await AddPlantToDatabase(gid, ch.Id, userId, msgId.Value, amount, pass); + await quests.ReportActionAsync(userId, QuestEventType.PlantOrPick, new() { { "type", "plant" } }); return true; } @@ -379,43 +371,42 @@ public class PlantPickService : IEService, IExecNoCommand, IReadyExecutor string pass, bool auto = false) { - await using var uow = _db.GetDbContext(); + await using var uow = db.GetDbContext(); PlantedCurrency[] deleted = []; if (!string.IsNullOrWhiteSpace(pass) && auto) { deleted = await uow.GetTable() - .Where(x => x.GuildId == gid - && x.ChannelId == cid - && x.Password != null - && x.Password.Length == pass.Length) - .DeleteWithOutputAsync(); + .Where(x => x.GuildId == gid + && x.ChannelId == cid + && x.Password != null + && x.Password.Length == pass.Length) + .DeleteWithOutputAsync(); } var totalDeletedAmount = deleted.Length == 0 ? 0 : deleted.Sum(x => x.Amount); await uow.GetTable() - .InsertAsync(() => new() - { - Amount = totalDeletedAmount + amount, - GuildId = gid, - ChannelId = cid, - Password = pass, - UserId = uid, - MessageId = mid, - }); + .InsertAsync(() => new() + { + Amount = totalDeletedAmount + amount, + GuildId = gid, + ChannelId = cid, + Password = pass, + UserId = uid, + MessageId = mid, + }); return (totalDeletedAmount + amount, deleted.Select(x => x.MessageId).ToArray()); } public async Task OnReadyAsync() { - await using var uow = _db.GetDbContext(); + await using var uow = db.GetDbContext(); _generationChannels = (await uow.GetTable() - .Select(x => x.ChannelId) - .ToListAsyncLinqToDB()) + .Select(x => x.ChannelId) + .ToListAsyncLinqToDB()) .ToHashSet() .ToConcurrentSet(); - } } \ No newline at end of file diff --git a/src/EllieBot/Modules/Gambling/Waifus/WaifuService.cs b/src/EllieBot/Modules/Gambling/Waifus/WaifuService.cs index e929ce9..47d49f9 100644 --- a/src/EllieBot/Modules/Gambling/Waifus/WaifuService.cs +++ b/src/EllieBot/Modules/Gambling/Waifus/WaifuService.cs @@ -6,6 +6,7 @@ using EllieBot.Common.ModuleBehaviors; using EllieBot.Db.Models; using EllieBot.Modules.Gambling.Common; using EllieBot.Modules.Gambling.Common.Waifu; +using EllieBot.Modules.Games.Quests; namespace EllieBot.Modules.Gambling.Services; @@ -15,23 +16,23 @@ public class WaifuService : IEService, IReadyExecutor private readonly ICurrencyService _cs; private readonly IBotCache _cache; private readonly GamblingConfigService _gss; - private readonly IBotCreds _creds; private readonly DiscordSocketClient _client; + private readonly QuestService _quests; public WaifuService( DbService db, ICurrencyService cs, IBotCache cache, GamblingConfigService gss, - IBotCreds creds, - DiscordSocketClient client) + DiscordSocketClient client, + QuestService quests) { _db = db; _cs = cs; _cache = cache; _gss = gss; - _creds = creds; _client = client; + _quests = quests; } public async Task WaifuTransfer(IUser owner, ulong waifuId, IUser newOwner) @@ -411,6 +412,8 @@ public class WaifuService : IEService, IReadyExecutor w.Price += (long)(totalValue * _gss.Data.Waifu.Multipliers.GiftEffect); else w.Price += totalValue / 2; + + await _quests.ReportActionAsync(from.Id, QuestEventType.WaifuGiftSent); } else { diff --git a/src/EllieBot/Modules/Games/Fish/FishService.cs b/src/EllieBot/Modules/Games/Fish/FishService.cs index 36dc20b..1213e55 100644 --- a/src/EllieBot/Modules/Games/Fish/FishService.cs +++ b/src/EllieBot/Modules/Games/Fish/FishService.cs @@ -4,6 +4,7 @@ using LinqToDB; using LinqToDB.EntityFrameworkCore; using EllieBot.Modules.Administration; using EllieBot.Modules.Administration.Services; +using EllieBot.Modules.Games.Quests; namespace EllieBot.Modules.Games.Fish; @@ -11,7 +12,8 @@ public sealed class FishService( FishConfigService fcs, IBotCache cache, DbService db, - INotifySubscriber notify + INotifySubscriber notify, + QuestService quests ) : IEService { @@ -91,6 +93,15 @@ public sealed class FishService( } } + await quests.ReportActionAsync(userId, + QuestEventType.FishCaught, + new() + { + { "fish", result.Fish.Name }, + { "type", typeRoll < nothingChance + fishChance ? "fish" : "trash" }, + { "stars", result.Stars.ToString() } + }); + return result; } diff --git a/src/EllieBot/Modules/Games/Hangman/HangmanService.cs b/src/EllieBot/Modules/Games/Hangman/HangmanService.cs index 8373e1b..c483027 100644 --- a/src/EllieBot/Modules/Games/Hangman/HangmanService.cs +++ b/src/EllieBot/Modules/Games/Hangman/HangmanService.cs @@ -2,6 +2,7 @@ using EllieBot.Common.ModuleBehaviors; using EllieBot.Modules.Games.Services; using System.Diagnostics.CodeAnalysis; +using EllieBot.Modules.Games.Quests; namespace EllieBot.Modules.Games.Hangman; @@ -13,6 +14,7 @@ public sealed class HangmanService : IHangmanService, IExecNoCommand private readonly GamesConfigService _gcs; private readonly ICurrencyService _cs; private readonly IMemoryCache _cdCache; + private readonly QuestService _quests; private readonly object _locker = new(); public HangmanService( @@ -20,13 +22,15 @@ public sealed class HangmanService : IHangmanService, IExecNoCommand IMessageSenderService sender, GamesConfigService gcs, ICurrencyService cs, - IMemoryCache cdCache) + IMemoryCache cdCache, + QuestService quests) { _source = source; _sender = sender; _gcs = gcs; _cs = cs; _cdCache = cdCache; + _quests = quests; } public bool StartHangman(ulong channelId, string? category, [NotNullWhen(true)] out HangmanGame.State? state) @@ -104,6 +108,9 @@ public sealed class HangmanService : IHangmanService, IExecNoCommand if (rew > 0) await _cs.AddAsync(msg.Author, rew, new("hangman", "win")); + + if (state.GuessResult == HangmanGame.GuessResult.Win) + await _quests.ReportActionAsync(msg.Author.Id, QuestEventType.GameWon, new() { { "game", "hangman" } }); await SendState((ITextChannel)msg.Channel, msg.Author, msg.Content, state); } diff --git a/src/EllieBot/Modules/Games/NCanvas/NCanvasService.cs b/src/EllieBot/Modules/Games/NCanvas/NCanvasService.cs index c0a4b1b..f76c7df 100644 --- a/src/EllieBot/Modules/Games/NCanvas/NCanvasService.cs +++ b/src/EllieBot/Modules/Games/NCanvas/NCanvasService.cs @@ -3,6 +3,7 @@ using LinqToDB.Data; using LinqToDB.EntityFrameworkCore; using EllieBot.Common.ModuleBehaviors; using EllieBot.Db.Models; +using EllieBot.Modules.Games.Quests; using SixLabors.ImageSharp.ColorSpaces; using SixLabors.ImageSharp.ColorSpaces.Conversion; using SixLabors.ImageSharp.PixelFormats; @@ -17,6 +18,7 @@ public sealed class NCanvasService : INCanvasService, IReadyExecutor, IEService private readonly IBotCache _cache; private readonly DiscordSocketClient _client; private readonly ICurrencyService _cs; + private readonly QuestService _quests; public const int CANVAS_WIDTH = 500; public const int CANVAS_HEIGHT = 350; @@ -26,12 +28,14 @@ public sealed class NCanvasService : INCanvasService, IReadyExecutor, IEService DbService db, IBotCache cache, DiscordSocketClient client, - ICurrencyService cs) + ICurrencyService cs, + QuestService quests) { _db = db; _cache = cache; _client = client; _cs = cs; + _quests = quests; } public async Task OnReadyAsync() @@ -59,23 +63,23 @@ public sealed class NCanvasService : INCanvasService, IReadyExecutor, IEService } await uow.GetTable() - .BulkCopyAsync(toAdd.Select(x => - { - var clr = ColorSpaceConverter.ToRgb(new Hsv(((float)Random.Shared.NextDouble() * 360), - (float)(0.5 + (Random.Shared.NextDouble() * 0.49)), - (float)(0.4 + (Random.Shared.NextDouble() / 5 + (x % 100 * 0.2))))) - .ToVector3(); + .BulkCopyAsync(toAdd.Select(x => + { + var clr = ColorSpaceConverter.ToRgb(new Hsv(((float)Random.Shared.NextDouble() * 360), + (float)(0.5 + (Random.Shared.NextDouble() * 0.49)), + (float)(0.4 + (Random.Shared.NextDouble() / 5 + (x % 100 * 0.2))))) + .ToVector3(); - var packed = new Rgba32(clr).PackedValue; - return new NCPixel() - { - Color = packed, - Price = 1, - Position = x, - Text = "", - OwnerId = 0 - }; - })); + var packed = new Rgba32(clr).PackedValue; + return new NCPixel() + { + Color = packed, + Price = 1, + Position = x, + Text = "", + OwnerId = 0 + }; + })); } @@ -83,9 +87,9 @@ public sealed class NCanvasService : INCanvasService, IReadyExecutor, IEService { await using var uow = _db.GetDbContext(); var colors = await uow.GetTable() - .OrderBy(x => x.Position) - .Select(x => x.Color) - .ToArrayAsyncLinqToDB(); + .OrderBy(x => x.Position) + .Select(x => x.Color) + .ToArrayAsyncLinqToDB(); return colors; } @@ -121,15 +125,15 @@ public sealed class NCanvasService : INCanvasService, IReadyExecutor, IEService { await using var uow = _db.GetDbContext(); var updates = await uow.GetTable() - .Where(x => x.Position == position && x.Price <= price) - .UpdateAsync(old => new NCPixel() - { - Position = position, - Color = color, - Text = text, - OwnerId = userId, - Price = price + 1 - }); + .Where(x => x.Position == position && x.Price <= price) + .UpdateAsync(old => new NCPixel() + { + Position = position, + Color = color, + Text = text, + OwnerId = userId, + Price = price + 1 + }); success = updates > 0; } catch @@ -140,6 +144,10 @@ public sealed class NCanvasService : INCanvasService, IReadyExecutor, IEService { await wallet.Add(price, new("canvas", "pixel-refund", $"Refund pixel {new kwum(position)} purchase")); } + else + { + await _quests.ReportActionAsync(userId, QuestEventType.PixelSet); + } return success ? SetPixelResult.Success : SetPixelResult.InsufficientPayment; } @@ -152,14 +160,14 @@ public sealed class NCanvasService : INCanvasService, IReadyExecutor, IEService await using var uow = _db.GetDbContext(); await uow.GetTable().DeleteAsync(); await uow.GetTable() - .BulkCopyAsync(colors.Select((x, i) => new NCPixel() - { - Color = x, - Price = INITIAL_PRICE, - Position = i, - Text = "", - OwnerId = 0 - })); + .BulkCopyAsync(colors.Select((x, i) => new NCPixel() + { + Color = x, + Price = INITIAL_PRICE, + Position = i, + Text = "", + OwnerId = 0 + })); return true; } @@ -190,12 +198,12 @@ public sealed class NCanvasService : INCanvasService, IReadyExecutor, IEService await using var uow = _db.GetDbContext(); return await uow.GetTable() - .Where(x => x.Position % CANVAS_WIDTH >= (position % CANVAS_WIDTH) - 2 - && x.Position % CANVAS_WIDTH <= (position % CANVAS_WIDTH) + 2 - && x.Position / CANVAS_WIDTH >= (position / CANVAS_WIDTH) - 2 - && x.Position / CANVAS_WIDTH <= (position / CANVAS_WIDTH) + 2) - .OrderBy(x => x.Position) - .ToArrayAsyncLinqToDB(); + .Where(x => x.Position % CANVAS_WIDTH >= (position % CANVAS_WIDTH) - 2 + && x.Position % CANVAS_WIDTH <= (position % CANVAS_WIDTH) + 2 + && x.Position / CANVAS_WIDTH >= (position / CANVAS_WIDTH) - 2 + && x.Position / CANVAS_WIDTH <= (position / CANVAS_WIDTH) + 2) + .OrderBy(x => x.Position) + .ToArrayAsyncLinqToDB(); } public int GetHeight() diff --git a/src/EllieBot/Modules/Games/Quests/Quest.cs b/src/EllieBot/Modules/Games/Quests/Quest.cs new file mode 100644 index 0000000..dae9552 --- /dev/null +++ b/src/EllieBot/Modules/Games/Quests/Quest.cs @@ -0,0 +1,9 @@ +namespace EllieBot.Modules.Games.Quests; + +public record class Quest( + QuestIds Id, + string Name, + string Description, + QuestEventType TriggerEvent, + int RequiredAmount +); \ No newline at end of file diff --git a/src/EllieBot/Modules/Games/Quests/QuestCommands.cs b/src/EllieBot/Modules/Games/Quests/QuestCommands.cs new file mode 100644 index 0000000..28adddd --- /dev/null +++ b/src/EllieBot/Modules/Games/Quests/QuestCommands.cs @@ -0,0 +1,38 @@ +namespace EllieBot.Modules.Games.Quests; + +public class QuestCommands : EllieModule +{ + [Cmd] + public async Task QuestLog() + { + var now = DateTime.UtcNow; + var quests = await _service.GetUserQuestsAsync(ctx.User.Id, now); + + var embed = CreateEmbed() + .WithOkColor() + .WithTitle(GetText(strs.quest_log)); + + var allDone = quests.All(x => x.UserQuest.IsCompleted); + + var tmrw = now.AddDays(1).Date; + var desc = GetText(strs.dailies_reset(TimestampTag.FromDateTime(tmrw, TimestampTagStyles.Relative))); + if (allDone) + desc = GetText(strs.dailies_done) + "\n" + desc; + + embed.WithDescription(desc); + + foreach (var res in quests) + { + if (res.Quest is null) + continue; + + embed.AddField( + (res.UserQuest.IsCompleted ? IQuest.COMPLETED : IQuest.INCOMPLETE) + " " + res.Quest.Name, + $"{res.Quest.Desc}\n\n" + + res.Quest.ToString(res.UserQuest.Progress), + true); + } + + await Response().Embed(embed).SendAsync(); + } +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Games/Quests/QuestEvent.cs b/src/EllieBot/Modules/Games/Quests/QuestEvent.cs new file mode 100644 index 0000000..973daa9 --- /dev/null +++ b/src/EllieBot/Modules/Games/Quests/QuestEvent.cs @@ -0,0 +1,15 @@ +namespace EllieBot.Modules.Games.Quests; + +public class QuestEvent +{ + public QuestEventType EventType { get; } + public ulong UserId { get; } + public Dictionary Metadata { get; } + + public QuestEvent(QuestEventType eventType, ulong userId, Dictionary? metadata = null) + { + EventType = eventType; + UserId = userId; + Metadata = metadata ?? new Dictionary(); + } +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Games/Quests/QuestEventType.cs b/src/EllieBot/Modules/Games/Quests/QuestEventType.cs new file mode 100644 index 0000000..b6dc78f --- /dev/null +++ b/src/EllieBot/Modules/Games/Quests/QuestEventType.cs @@ -0,0 +1,15 @@ +namespace EllieBot.Modules.Games.Quests; + +public enum QuestEventType +{ + CommandUsed, + GameWon, + BetPlaced, + FishCaught, + PixelSet, + RaceJoined, + BankAction, + PlantOrPick, + Give, + WaifuGiftSent +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Games/Quests/QuestIds.cs b/src/EllieBot/Modules/Games/Quests/QuestIds.cs new file mode 100644 index 0000000..16e6bbd --- /dev/null +++ b/src/EllieBot/Modules/Games/Quests/QuestIds.cs @@ -0,0 +1,16 @@ +namespace EllieBot.Modules.Games.Quests; + +public enum QuestIds +{ + HangmanWin, + Bet, + WaifuGift, + CatchFish, + SetPixels, + JoinAnimalRace, + BankDeposit, + CheckBetting, + PlantPick, + GiveFlowers, + WellInformed +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Games/Quests/QuestModels/BankerQuest.cs b/src/EllieBot/Modules/Games/Quests/QuestModels/BankerQuest.cs new file mode 100644 index 0000000..09c8b30 --- /dev/null +++ b/src/EllieBot/Modules/Games/Quests/QuestModels/BankerQuest.cs @@ -0,0 +1,65 @@ +namespace EllieBot.Modules.Games.Quests; + +public sealed class BankerQuest : IQuest +{ + public QuestIds QuestId + => QuestIds.BankDeposit; + + public string Name + => "Banker"; + + public string Desc + => "Perform bank actions"; + + public string ProgDesc + => ""; + + public QuestEventType EventType + => QuestEventType.BankAction; + + public long RequiredAmount + => 0b111; + + public long TryUpdateProgress(IDictionary metadata, long oldProgress) + { + if (!metadata.TryGetValue("type", out var type)) + return oldProgress; + + var progress = oldProgress; + + if (type == "balance") + progress |= 0b001; + else if (type == "deposit") + progress |= 0b010; + else if (type == "withdraw") + progress |= 0b100; + + return progress; + } + + public string ToString(long progress) + { + var msg = ""; + + var emoji = IQuest.INCOMPLETE; + if ((progress & 0b001) == 0b001) + emoji = IQuest.COMPLETED; + + msg += emoji + " checked bank balance"; + + emoji = IQuest.INCOMPLETE; + if ((progress & 0b010) == 0b010) + emoji = IQuest.COMPLETED; + + msg += "\n" + emoji + " made a deposit"; + + emoji = IQuest.INCOMPLETE; + if ((progress & 0b100) == 0b100) + emoji = IQuest.COMPLETED; + + msg += "\n" + emoji + " made a withdrawal"; + + return msg; + } + +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Games/Quests/QuestModels/BetFlowersQuest.cs b/src/EllieBot/Modules/Games/Quests/QuestModels/BetFlowersQuest.cs new file mode 100644 index 0000000..0cb146f --- /dev/null +++ b/src/EllieBot/Modules/Games/Quests/QuestModels/BetFlowersQuest.cs @@ -0,0 +1,31 @@ +namespace EllieBot.Modules.Games.Quests; + +public sealed class BetFlowersQuest : IQuest +{ + public QuestIds QuestId + => QuestIds.Bet; + + public string Name + => "Flower Gambler"; + + public string Desc + => "Bet 300 flowers"; + + public string ProgDesc + => "flowers bet"; + + public QuestEventType EventType + => QuestEventType.BetPlaced; + + public long RequiredAmount + => 300; + + public long TryUpdateProgress(IDictionary metadata, long oldProgress) + { + if (!metadata.TryGetValue("amount", out var amountStr) + || !long.TryParse(amountStr, out var amount)) + return oldProgress; + + return oldProgress + amount; + } +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Games/Quests/QuestModels/BetQuest.cs b/src/EllieBot/Modules/Games/Quests/QuestModels/BetQuest.cs new file mode 100644 index 0000000..23ffbed --- /dev/null +++ b/src/EllieBot/Modules/Games/Quests/QuestModels/BetQuest.cs @@ -0,0 +1,27 @@ +namespace EllieBot.Modules.Games.Quests; + +public sealed class BetQuest : IQuest +{ + public QuestIds QuestId + => QuestIds.Bet; + + public string Name + => "High Roller"; + + public string Desc + => "Place 10 bets"; + + public string ProgDesc + => "bets placed"; + + public QuestEventType EventType + => QuestEventType.BetPlaced; + + public long RequiredAmount + => 10; + + public long TryUpdateProgress(IDictionary metadata, long oldProgress) + { + return oldProgress + 1; + } +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Games/Quests/QuestModels/CatchFishQuest.cs b/src/EllieBot/Modules/Games/Quests/QuestModels/CatchFishQuest.cs new file mode 100644 index 0000000..010426a --- /dev/null +++ b/src/EllieBot/Modules/Games/Quests/QuestModels/CatchFishQuest.cs @@ -0,0 +1,30 @@ +namespace EllieBot.Modules.Games.Quests; + +public sealed class CatchFishQuest : IQuest +{ + public QuestIds QuestId + => QuestIds.CatchFish; + + public string Name + => "Fisherman"; + + public string Desc + => "Catch 5 fish"; + + public string ProgDesc + => "fish caught"; + + public QuestEventType EventType + => QuestEventType.FishCaught; + + public long RequiredAmount + => 5; + + public long TryUpdateProgress(IDictionary metadata, long oldProgress) + { + if (metadata.TryGetValue("type", out var type) && type == "fish") + return oldProgress + 1; + + return oldProgress; + } +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Games/Quests/QuestModels/CatchQualityQuest.cs b/src/EllieBot/Modules/Games/Quests/QuestModels/CatchQualityQuest.cs new file mode 100644 index 0000000..fd4ec38 --- /dev/null +++ b/src/EllieBot/Modules/Games/Quests/QuestModels/CatchQualityQuest.cs @@ -0,0 +1,32 @@ +namespace EllieBot.Modules.Games.Quests; + +public sealed class CatchQualityQuest : IQuest +{ + public QuestIds QuestId + => QuestIds.CatchFish; + + public string Name + => "Master Angler"; + + public string Desc + => "Catch a fish or an item rated 3 stars or above."; + + public string ProgDesc + => "3+ star fish caught"; + + public QuestEventType EventType + => QuestEventType.FishCaught; + + public long RequiredAmount + => 1; + + public long TryUpdateProgress(IDictionary metadata, long oldProgress) + { + if (metadata.TryGetValue("stars", out var quality) + && int.TryParse(quality, out var q) + && q >= 3) + return oldProgress + 1; + + return oldProgress; + } +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Games/Quests/QuestModels/CatchTrashQuest.cs b/src/EllieBot/Modules/Games/Quests/QuestModels/CatchTrashQuest.cs new file mode 100644 index 0000000..3f54d93 --- /dev/null +++ b/src/EllieBot/Modules/Games/Quests/QuestModels/CatchTrashQuest.cs @@ -0,0 +1,30 @@ +namespace EllieBot.Modules.Games.Quests; + +public sealed class CatchTrashQuest : IQuest +{ + public QuestIds QuestId + => QuestIds.CatchFish; + + public string Name + => "Environmentalist"; + + public string Desc + => "Catch 5 trash items while fishing"; + + public string ProgDesc + => "items caught"; + + public QuestEventType EventType + => QuestEventType.FishCaught; + + public long RequiredAmount + => 5; + + public long TryUpdateProgress(IDictionary metadata, long oldProgress) + { + if (metadata.TryGetValue("type", out var type) && type == "trash") + return oldProgress + 1; + + return oldProgress; + } +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Games/Quests/QuestModels/CheckLeaderboardsQuest.cs b/src/EllieBot/Modules/Games/Quests/QuestModels/CheckLeaderboardsQuest.cs new file mode 100644 index 0000000..4b9f9b0 --- /dev/null +++ b/src/EllieBot/Modules/Games/Quests/QuestModels/CheckLeaderboardsQuest.cs @@ -0,0 +1,64 @@ +namespace EllieBot.Modules.Games.Quests; + +public sealed class CheckLeaderboardsQuest : IQuest +{ + public QuestIds QuestId + => QuestIds.CheckBetting; + + public string Name + => "Leaderboard Enthusiast"; + + public string Desc + => "Check lb, xplb and waifulb"; + + public string ProgDesc + => ""; + + public QuestEventType EventType + => QuestEventType.CommandUsed; + + public long RequiredAmount + => 0b111; + + public long TryUpdateProgress(IDictionary metadata, long oldProgress) + { + if (!metadata.TryGetValue("name", out var name)) + return oldProgress; + + var progress = oldProgress; + + if (name == "leaderboard") + progress |= 0b001; + else if (name == "xpleaderboard") + progress |= 0b010; + else if (name == "waifulb") + progress |= 0b100; + + return progress; + } + + public string ToString(long progress) + { + var msg = ""; + + var emoji = IQuest.INCOMPLETE; + if ((progress & 0b001) == 0b001) + emoji = IQuest.COMPLETED; + + msg += emoji + " flower lb seen\n"; + + emoji = IQuest.INCOMPLETE; + if ((progress & 0b010) == 0b010) + emoji = IQuest.COMPLETED; + + msg += emoji + " xp lb seen\n"; + + emoji = IQuest.INCOMPLETE; + if ((progress & 0b100) == 0b100) + emoji = IQuest.COMPLETED; + + msg += emoji + " waifu lb seen"; + + return msg; + } +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Games/Quests/QuestModels/GiftWaifuQuest.cs b/src/EllieBot/Modules/Games/Quests/QuestModels/GiftWaifuQuest.cs new file mode 100644 index 0000000..a6c5b0b --- /dev/null +++ b/src/EllieBot/Modules/Games/Quests/QuestModels/GiftWaifuQuest.cs @@ -0,0 +1,27 @@ +namespace EllieBot.Modules.Games.Quests; + +public sealed class GiftWaifuQuest : IQuest +{ + public QuestIds QuestId + => QuestIds.WaifuGift; + + public string Name + => "Generous Gifter"; + + public string Desc + => "Gift a waifu"; + + public string ProgDesc + => "waifus gifted"; + + public QuestEventType EventType + => QuestEventType.WaifuGiftSent; + + public long RequiredAmount + => 1; + + public long TryUpdateProgress(IDictionary metadata, long oldProgress) + { + return oldProgress + 1; + } +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Games/Quests/QuestModels/GiveFlowersQuest.cs b/src/EllieBot/Modules/Games/Quests/QuestModels/GiveFlowersQuest.cs new file mode 100644 index 0000000..54987c7 --- /dev/null +++ b/src/EllieBot/Modules/Games/Quests/QuestModels/GiveFlowersQuest.cs @@ -0,0 +1,31 @@ +namespace EllieBot.Modules.Games.Quests; + +public sealed class GiveFlowersQuest : IQuest +{ + public QuestIds QuestId + => QuestIds.GiveFlowers; + + public string Name + => "Sharing is Caring"; + + public string Desc + => "Give 10 flowers to someone"; + + public string ProgDesc + => "flowers given"; + + public QuestEventType EventType + => QuestEventType.Give; + + public long RequiredAmount + => 10; + + public long TryUpdateProgress(IDictionary metadata, long oldProgress) + { + if (!metadata.TryGetValue("amount", out var amountStr) + || !long.TryParse(amountStr, out var amount)) + return oldProgress; + + return oldProgress + amount; + } +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Games/Quests/QuestModels/HangmanWinQuest.cs b/src/EllieBot/Modules/Games/Quests/QuestModels/HangmanWinQuest.cs new file mode 100644 index 0000000..72c2ad1 --- /dev/null +++ b/src/EllieBot/Modules/Games/Quests/QuestModels/HangmanWinQuest.cs @@ -0,0 +1,30 @@ +namespace EllieBot.Modules.Games.Quests; + +public sealed class HangmanWinQuest : IQuest +{ + public QuestIds QuestId + => QuestIds.HangmanWin; + + public string Name + => "Hangman Champion"; + + public string Desc + => "Win a game of Hangman"; + + public string ProgDesc + => "hangman games won"; + + public QuestEventType EventType + => QuestEventType.GameWon; + + public long RequiredAmount + => 1; + + public long TryUpdateProgress(IDictionary metadata, long oldProgress) + { + if (!metadata.TryGetValue("game", out var value)) + return oldProgress; + + return value == "hangman" ? oldProgress + 1 : oldProgress; + } +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Games/Quests/QuestModels/IQuest.cs b/src/EllieBot/Modules/Games/Quests/QuestModels/IQuest.cs new file mode 100644 index 0000000..935498f --- /dev/null +++ b/src/EllieBot/Modules/Games/Quests/QuestModels/IQuest.cs @@ -0,0 +1,33 @@ +using EllieBot.Db.Models; + +namespace EllieBot.Modules.Games.Quests; + +public interface IQuest +{ + QuestIds QuestId { get; } + string Name { get; } + string Desc { get; } + string ProgDesc { get; } + QuestEventType EventType { get; } + long RequiredAmount { get; } + + public long TryUpdateProgress(IDictionary metadata, long oldProgress); + + public virtual string ToString(long progress) + => GetEmoji(progress, RequiredAmount) + $" [{progress}/{RequiredAmount}] " + ProgDesc; + + public static string GetEmoji(long progress, long requiredAmount) + => progress >= requiredAmount + ? COMPLETED + : INCOMPLETE; + + /// + /// Completed Emoji + /// + public const string COMPLETED = "\\✅"; + + /// + /// Incomplete Emoji + /// + public const string INCOMPLETE = "\\❌"; +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Games/Quests/QuestModels/JoinAnimalRaceQuest.cs b/src/EllieBot/Modules/Games/Quests/QuestModels/JoinAnimalRaceQuest.cs new file mode 100644 index 0000000..137ed15 --- /dev/null +++ b/src/EllieBot/Modules/Games/Quests/QuestModels/JoinAnimalRaceQuest.cs @@ -0,0 +1,27 @@ +namespace EllieBot.Modules.Games.Quests; + +public sealed class JoinAnimalRaceQuest : IQuest +{ + public QuestIds QuestId + => QuestIds.JoinAnimalRace; + + public string Name + => "Race Participant"; + + public string Desc + => "Join an animal race"; + + public string ProgDesc + => "races joined"; + + public QuestEventType EventType + => QuestEventType.RaceJoined; + + public long RequiredAmount + => 1; + + public long TryUpdateProgress(IDictionary metadata, long oldProgress) + { + return oldProgress + 1; + } +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Games/Quests/QuestModels/PlantPickQuest.cs b/src/EllieBot/Modules/Games/Quests/QuestModels/PlantPickQuest.cs new file mode 100644 index 0000000..7004f2e --- /dev/null +++ b/src/EllieBot/Modules/Games/Quests/QuestModels/PlantPickQuest.cs @@ -0,0 +1,61 @@ +namespace EllieBot.Modules.Games.Quests; + +public sealed class PlantPickQuest : IQuest +{ + public QuestIds QuestId + => QuestIds.PlantPick; + + public string Name + => "Gardener"; + + public string Desc + => "pick and plant"; + + public string ProgDesc + => ""; + + public QuestEventType EventType + => QuestEventType.PlantOrPick; + + public long RequiredAmount + => 0b11; + + public long TryUpdateProgress(IDictionary metadata, long oldProgress) + { + if (!metadata.TryGetValue("type", out var val)) + return oldProgress; + + if (val == "plant") + { + oldProgress |= 0b10; + return oldProgress; + } + + if (val == "pick") + { + oldProgress |= 0b01; + return oldProgress; + } + + return oldProgress; + } + + public string ToString(long progress) + { + var msg = ""; + + var emoji = IQuest.INCOMPLETE; + if ((progress & 0b01) == 0b01) + emoji = IQuest.COMPLETED; + + msg += emoji + " picked flowers\n"; + + emoji = IQuest.INCOMPLETE; + if ((progress & 0b10) == 0b10) + emoji = IQuest.COMPLETED; + + msg += emoji + " planted flowers"; + + return msg; + } +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Games/Quests/QuestModels/SetPixelsQuest.cs b/src/EllieBot/Modules/Games/Quests/QuestModels/SetPixelsQuest.cs new file mode 100644 index 0000000..e704672 --- /dev/null +++ b/src/EllieBot/Modules/Games/Quests/QuestModels/SetPixelsQuest.cs @@ -0,0 +1,27 @@ +namespace EllieBot.Modules.Games.Quests; + +public sealed class SetPixelsQuest : IQuest +{ + public QuestIds QuestId + => QuestIds.SetPixels; + + public string Name + => "Pixel Artist"; + + public string Desc + => "Set 3 pixels"; + + public string ProgDesc + => "pixels set"; + + public QuestEventType EventType + => QuestEventType.PixelSet; + + public long RequiredAmount + => 3; + + public long TryUpdateProgress(IDictionary metadata, long oldProgress) + { + return oldProgress + 1; + } +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Games/Quests/QuestModels/WellInformed.cs b/src/EllieBot/Modules/Games/Quests/QuestModels/WellInformed.cs new file mode 100644 index 0000000..f34a4e4 --- /dev/null +++ b/src/EllieBot/Modules/Games/Quests/QuestModels/WellInformed.cs @@ -0,0 +1,64 @@ +namespace EllieBot.Modules.Games.Quests; + +public sealed class WellInformedQuest : IQuest +{ + public QuestIds QuestId + => QuestIds.WellInformed; + + public string Name + => "Well Informed"; + + public string Desc + => "Check your flower stats"; + + public string ProgDesc + => ""; + + public QuestEventType EventType + => QuestEventType.CommandUsed; + + public long RequiredAmount + => 0b111; + + public long TryUpdateProgress(IDictionary metadata, long oldProgress) + { + if (!metadata.TryGetValue("name", out var type)) + return oldProgress; + + var progress = oldProgress; + + if (type == "cash") + progress |= 0b001; + else if (type == "rakeback") + progress |= 0b010; + else if (type == "betstats") + progress |= 0b100; + + return progress; + } + + public string ToString(long progress) + { + var msg = ""; + + var emoji = IQuest.INCOMPLETE; + if ((progress & 0b001) == 0b001) + emoji = IQuest.COMPLETED; + + msg += emoji + " checked cash\n"; + + emoji = IQuest.INCOMPLETE; + if ((progress & 0b010) == 0b010) + emoji = IQuest.COMPLETED; + + msg += emoji + " checked rakeback\n"; + + emoji = IQuest.INCOMPLETE; + if ((progress & 0b100) == 0b100) + emoji = IQuest.COMPLETED; + + msg += emoji + " checked bet stats"; + + return msg; + } +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Games/Quests/QuestService.cs b/src/EllieBot/Modules/Games/Quests/QuestService.cs new file mode 100644 index 0000000..c251384 --- /dev/null +++ b/src/EllieBot/Modules/Games/Quests/QuestService.cs @@ -0,0 +1,206 @@ +using LinqToDB; +using LinqToDB.EntityFrameworkCore; +using Microsoft.CodeAnalysis.Operations; +using EllieBot.Common.ModuleBehaviors; +using EllieBot.Db.Models; + +namespace EllieBot.Modules.Games.Quests; + +public sealed class QuestService( + DbService db, + IBotCache botCache, + IMessageSenderService sender, + DiscordSocketClient client +) : IEService, IExecPreCommand +{ + private readonly EllieRandom rng = new(); + + private readonly IQuest[] _availableQuests = + [ + new HangmanWinQuest(), + new PlantPickQuest(), + new BetQuest(), + new BetFlowersQuest(), + new GiftWaifuQuest(), + new CatchFishQuest(), + new SetPixelsQuest(), + new JoinAnimalRaceQuest(), + new BankerQuest(), + new CheckLeaderboardsQuest(), + new WellInformedQuest(), + ]; + + private const int MAX_QUESTS_PER_DAY = 3; + + private TypedKey UserHasQuestsKey(ulong userId) + => new($"daily:generated:{userId}"); + + private TypedKey UserCompletedDailiesKey(ulong userId) + => new($"daily:completed:{userId}"); + + + public Task ReportActionAsync( + ulong userId, + QuestEventType eventType, + Dictionary? metadata = null) + { + // don't block any caller + + _ = Task.Run(async () => + { + Log.Information("Action reported by {UserId}: {EventType} {Metadata}", + userId, + eventType, + metadata.ToJson()); + metadata ??= new(); + var now = DateTime.UtcNow; + + var alreadyDone = await botCache.GetAsync(UserCompletedDailiesKey(userId)); + if (alreadyDone.IsT0) + return; + + var userQuests = await GetUserQuestsAsync(userId, now); + + foreach (var (q, uq) in userQuests) + { + // deleted quest + if (q is null) + continue; + + // user already completed or incorrect event + if (uq.IsCompleted || q.EventType != eventType) + continue; + + var newProgress = q.TryUpdateProgress(metadata, uq.Progress); + + // user already did that part of the quest + if (newProgress == uq.Progress) + continue; + + var isCompleted = newProgress >= q.RequiredAmount; + + await using var uow = db.GetDbContext(); + await uow.GetTable() + .Where(x => x.UserId == userId && x.QuestId == q.QuestId && x.QuestNumber == uq.QuestNumber) + .Set(x => x.Progress, newProgress) + .Set(x => x.IsCompleted, isCompleted) + .UpdateAsync(); + + uq.IsCompleted = isCompleted; + + if (userQuests.All(x => x.UserQuest.IsCompleted)) + { + var timeUntilTomorrow = now.Date.AddDays(1) - DateTime.UtcNow; + if (!await botCache.AddAsync( + UserCompletedDailiesKey(userId), + true, + expiry: timeUntilTomorrow)) + return; + + try + { + var user = await client.GetUserAsync(userId); + await sender + .Response(user) + .Confirm(strs.dailies_done) + .SendAsync(); + } + catch + { + // we don't really care if the user receives it + } + + break; + } + } + }); + + return Task.CompletedTask; + } + + public async Task> GetUserQuestsAsync( + ulong userId, + DateTime now) + { + var today = now.Date; + await EnsureUserDailiesAsync(userId, today); + + await using var uow = db.GetDbContext(); + var quests = await uow.GetTable() + .Where(x => x.UserId == userId && x.DateAssigned == today) + .ToListAsync(); + + return quests + .Select(x => (_availableQuests.FirstOrDefault(q => q.QuestId == x.QuestId), x)) + .Select(x => x!) + .ToList(); + } + + private async Task EnsureUserDailiesAsync(ulong userId, DateTime date) + { + var today = date.Date; + var timeUntilTomorrow = today.AddDays(1) - DateTime.UtcNow; + if (!await botCache.AddAsync(UserHasQuestsKey(userId), true, expiry: timeUntilTomorrow)) + return; + + await using var uow = db.GetDbContext(); + var newQuests = GenerateDailyQuestsAsync(userId); + for (var i = 0; i < MAX_QUESTS_PER_DAY; i++) + { + await uow.GetTable() + .InsertOrUpdateAsync(() => new() + { + UserId = userId, + QuestNumber = i, + DateAssigned = today, + + IsCompleted = false, + QuestId = newQuests[i].QuestId, + Progress = 0, + }, + old => new() + { + }, + () => new() + { + UserId = userId, + QuestNumber = i, + DateAssigned = today + }); + } + } + + private IReadOnlyList GenerateDailyQuestsAsync(ulong userId) + { + return _availableQuests + .ToList() + .Shuffle() + .Take(MAX_QUESTS_PER_DAY) + .ToList(); + } + + public int Priority + => int.MinValue; + + public async Task ExecPreCommandAsync(ICommandContext context, string moduleName, CommandInfo command) + { + var cmdName = command.Name.ToLowerInvariant(); + + await ReportActionAsync( + context.User.Id, + QuestEventType.CommandUsed, + new() + { + { "name", cmdName } + }); + + return false; + } + + public async Task UserCompletedDailies(ulong userId) + { + var result = await botCache.GetAsync(UserCompletedDailiesKey(userId)); + + return result.IsT0; + } +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Games/Quests/db/UserQuest.cs b/src/EllieBot/Modules/Games/Quests/db/UserQuest.cs new file mode 100644 index 0000000..bc95ad9 --- /dev/null +++ b/src/EllieBot/Modules/Games/Quests/db/UserQuest.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; +using EllieBot.Modules.Games.Quests; + +namespace EllieBot.Db.Models; + +public class UserQuest +{ + [Key] + public int Id { get; set; } + + public int QuestNumber { get; set; } + public ulong UserId { get; set; } + + public QuestIds QuestId { get; set; } + + public int Progress { get; set; } + + public bool IsCompleted { get; set; } + + public DateTime DateAssigned { get; set; } +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Owner/OwnerCommands.cs b/src/EllieBot/Modules/Owner/OwnerCommands.cs index 09d68f8..6ed996f 100644 --- a/src/EllieBot/Modules/Owner/OwnerCommands.cs +++ b/src/EllieBot/Modules/Owner/OwnerCommands.cs @@ -12,7 +12,7 @@ public class Owner(VoteRewardService vrs) : EllieModule await ctx.OkAsync(); } - private static CancellationTokenSource _cts = null; + private static CancellationTokenSource? _cts = null; [Cmd] public async Task MassPing() @@ -22,6 +22,8 @@ public class Owner(VoteRewardService vrs) : EllieModule await t.CancelAsync(); } + _cts = new(); + try { var users = await ctx.Guild.GetUsersAsync().Fmap(u => u.Where(x => !x.IsBot).ToArray()); diff --git a/src/EllieBot/_common/Currency/ICurrencyService.cs b/src/EllieBot/_common/Currency/ICurrencyService.cs index 4fe8b5c..9e4aa1d 100644 --- a/src/EllieBot/_common/Currency/ICurrencyService.cs +++ b/src/EllieBot/_common/Currency/ICurrencyService.cs @@ -38,7 +38,7 @@ public interface ICurrencyService IUser user, long amount, TxData? txData); - + Task> GetTopRichest(ulong ignoreId, int page = 0, int perPage = 9); Task> GetTransactionsAsync( @@ -47,4 +47,14 @@ public interface ICurrencyService int perPage = 15); Task GetTransactionsCountAsync(ulong userId); + + Task TransferAsync( + IMessageSenderService sender, + IUser from, + IUser to, + long amount, + string? note, + string formattedAmount); + + Task GetBalanceAsync(ulong userId); } \ No newline at end of file diff --git a/src/EllieBot/_common/Services/Currency/CurrencyService.cs b/src/EllieBot/_common/Services/Currency/CurrencyService.cs index 974febd..f730f3c 100644 --- a/src/EllieBot/_common/Services/Currency/CurrencyService.cs +++ b/src/EllieBot/_common/Services/Currency/CurrencyService.cs @@ -6,21 +6,12 @@ using EllieBot.Services.Currency; namespace EllieBot.Services; -public sealed class CurrencyService : ICurrencyService, IEService +public sealed class CurrencyService(DbService db, ITxTracker txTracker) : ICurrencyService, IEService { - private readonly DbService _db; - private readonly ITxTracker _txTracker; - - public CurrencyService(DbService db, ITxTracker txTracker) - { - _db = db; - _txTracker = txTracker; - } - public Task GetWalletAsync(ulong userId, CurrencyType type = CurrencyType.Default) { if (type == CurrencyType.Default) - return Task.FromResult(new DefaultWallet(userId, _db)); + return Task.FromResult(new DefaultWallet(userId, db)); throw new ArgumentOutOfRangeException(nameof(type)); } @@ -53,16 +44,16 @@ public sealed class CurrencyService : ICurrencyService, IEService { if (type == CurrencyType.Default) { - await using var ctx = _db.GetDbContext(); + await using var ctx = db.GetDbContext(); await ctx - .GetTable() - .Where(x => userIds.Contains(x.UserId)) - .UpdateAsync(du => new() - { - CurrencyAmount = du.CurrencyAmount >= amount - ? du.CurrencyAmount - amount - : 0 - }); + .GetTable() + .Where(x => userIds.Contains(x.UserId)) + .UpdateAsync(du => new() + { + CurrencyAmount = du.CurrencyAmount >= amount + ? du.CurrencyAmount - amount + : 0 + }); await ctx.SaveChangesAsync(); return; } @@ -77,7 +68,7 @@ public sealed class CurrencyService : ICurrencyService, IEService { var wallet = await GetWalletAsync(userId); await wallet.Add(amount, txData); - await _txTracker.TrackAdd(userId, amount, txData); + await txTracker.TrackAdd(userId, amount, txData); } public async Task AddAsync( @@ -97,7 +88,7 @@ public sealed class CurrencyService : ICurrencyService, IEService var wallet = await GetWalletAsync(userId); var result = await wallet.Take(amount, txData); if (result) - await _txTracker.TrackRemove(userId, amount, txData); + await txTracker.TrackRemove(userId, amount, txData); return result; } @@ -109,7 +100,7 @@ public sealed class CurrencyService : ICurrencyService, IEService public async Task> GetTopRichest(ulong ignoreId, int page = 0, int perPage = 9) { - await using var uow = _db.GetDbContext(); + await using var uow = db.GetDbContext(); return await uow.Set().GetTopRichest(ignoreId, page, perPage); } @@ -118,23 +109,63 @@ public sealed class CurrencyService : ICurrencyService, IEService int page, int perPage = 15) { - await using var uow = _db.GetDbContext(); + await using var uow = db.GetDbContext(); var trs = await uow.GetTable() - .Where(x => x.UserId == userId) - .OrderByDescending(x => x.DateAdded) - .Skip(perPage * page) - .Take(perPage) - .ToListAsyncLinqToDB(); + .Where(x => x.UserId == userId) + .OrderByDescending(x => x.DateAdded) + .Skip(perPage * page) + .Take(perPage) + .ToListAsyncLinqToDB(); return trs; } public async Task GetTransactionsCountAsync(ulong userId) { - await using var uow = _db.GetDbContext(); + await using var uow = db.GetDbContext(); return await uow.GetTable() - .Where(x => x.UserId == userId) - .CountAsyncLinqToDB(); + .Where(x => x.UserId == userId) + .CountAsyncLinqToDB(); + } + + public async Task TransferAsync( + IMessageSenderService sender, + IUser from, + IUser to, + long amount, + string note, + string formattedAmount) + { + var fromWallet = await GetWalletAsync(from.Id); + var toWallet = await GetWalletAsync(to.Id); + + var extra = new TxData("gift", from.ToString()!, note, from.Id); + + if (await fromWallet.Transfer(amount, toWallet, extra)) + { + try + { + await sender.Response(to) + .Confirm(string.IsNullOrWhiteSpace(note) + ? $"Received {formattedAmount} from {from} " + : $"Received {formattedAmount} from {from}: {note}") + .SendAsync(); + } + catch + { + //ignored + } + + return true; + } + + return false; + } + + public async Task GetBalanceAsync(ulong userId) + { + var wallet = await GetWalletAsync(userId); + return await wallet.GetBalance(); } } \ No newline at end of file diff --git a/src/EllieBot/_common/Services/Currency/CurrencyServiceExtensions.cs b/src/EllieBot/_common/Services/Currency/CurrencyServiceExtensions.cs index 7007ee4..2a717f3 100644 --- a/src/EllieBot/_common/Services/Currency/CurrencyServiceExtensions.cs +++ b/src/EllieBot/_common/Services/Currency/CurrencyServiceExtensions.cs @@ -4,45 +4,8 @@ namespace EllieBot.Services; public static class CurrencyServiceExtensions { - public static async Task GetBalanceAsync(this ICurrencyService cs, ulong userId) - { - var wallet = await cs.GetWalletAsync(userId); - return await wallet.GetBalance(); - } - + + // FUTURE should be a transaction - public static async Task TransferAsync( - this ICurrencyService cs, - IMessageSenderService sender, - IUser from, - IUser to, - long amount, - string? note, - string formattedAmount) - { - var fromWallet = await cs.GetWalletAsync(from.Id); - var toWallet = await cs.GetWalletAsync(to.Id); - - var extra = new TxData("gift", from.ToString()!, note, from.Id); - - if (await fromWallet.Transfer(amount, toWallet, extra)) - { - try - { - await sender.Response(to) - .Confirm(string.IsNullOrWhiteSpace(note) - ? $"Received {formattedAmount} from {from} " - : $"Received {formattedAmount} from {from}: {note}") - .SendAsync(); - } - catch - { - //ignored - } - - return true; - } - - return false; - } + } \ No newline at end of file diff --git a/src/EllieBot/_common/Services/Currency/GamblingTxTracker.cs b/src/EllieBot/_common/Services/Currency/GamblingTxTracker.cs index 92a0fda..3441e14 100644 --- a/src/EllieBot/_common/Services/Currency/GamblingTxTracker.cs +++ b/src/EllieBot/_common/Services/Currency/GamblingTxTracker.cs @@ -8,10 +8,15 @@ using EllieBot.Modules.Gambling; using System.Collections.Concurrent; using EllieBot.Modules.Administration; using EllieBot.Modules.Gambling.Services; +using EllieBot.Modules.Games.Quests; namespace EllieBot.Services; -public sealed class GamblingTxTracker : ITxTracker, IEService, IReadyExecutor +public sealed class GamblingTxTracker( + DbService db, + QuestService quests +) + : ITxTracker, IEService, IReadyExecutor { private static readonly IReadOnlySet _gamblingTypes = new HashSet(new[] { @@ -21,17 +26,6 @@ public sealed class GamblingTxTracker : ITxTracker, IEService, IReadyExecutor private NonBlocking.ConcurrentDictionary globalStats = new(); private ConcurrentBag userStats = new(); - private readonly DbService _db; - private readonly GamblingConfigService _gcs; - private readonly INotifySubscriber _notify; - - public GamblingTxTracker(DbService db, GamblingConfigService gcs, INotifySubscriber notify) - { - _db = db; - _gcs = gcs; - _notify = notify; - } - public async Task OnReadyAsync() => await Task.WhenAll(RunUserStatsCollector(), RunBetStatsCollector()); @@ -40,7 +34,7 @@ public sealed class GamblingTxTracker : ITxTracker, IEService, IReadyExecutor using var timer = new PeriodicTimer(TimeSpan.FromHours(1)); while (await timer.WaitForNextTickAsync()) { - await using var ctx = _db.GetDbContext(); + await using var ctx = db.GetDbContext(); try { @@ -51,22 +45,22 @@ public sealed class GamblingTxTracker : ITxTracker, IEService, IReadyExecutor if (globalStats.TryRemove(key, out var stat)) { await ctx.GetTable() - .InsertOrUpdateAsync(() => new() - { - Feature = key, - Bet = stat.Bet, - PaidOut = stat.PaidOut, - DateAdded = DateTime.UtcNow - }, - old => new() - { - Bet = old.Bet + stat.Bet, - PaidOut = old.PaidOut + stat.PaidOut, - }, - () => new() - { - Feature = key - }); + .InsertOrUpdateAsync(() => new() + { + Feature = key, + Bet = stat.Bet, + PaidOut = stat.PaidOut, + DateAdded = DateTime.UtcNow + }, + old => new() + { + Bet = old.Bet + stat.Bet, + PaidOut = old.PaidOut + stat.PaidOut, + }, + () => new() + { + Feature = key + }); } } } @@ -100,68 +94,68 @@ public sealed class GamblingTxTracker : ITxTracker, IEService, IReadyExecutor // update userstats foreach (var (k, x) in users.GroupBy(x => (x.UserId, x.Game)) - .ToDictionary(x => x.Key, - x => x.Aggregate((a, b) => new() - { - WinCount = a.WinCount + b.WinCount, - LoseCount = a.LoseCount + b.LoseCount, - TotalBet = a.TotalBet + b.TotalBet, - PaidOut = a.PaidOut + b.PaidOut, - MaxBet = Math.Max(a.MaxBet, b.MaxBet), - MaxWin = Math.Max(a.MaxWin, b.MaxWin), - }))) + .ToDictionary(x => x.Key, + x => x.Aggregate((a, b) => new() + { + WinCount = a.WinCount + b.WinCount, + LoseCount = a.LoseCount + b.LoseCount, + TotalBet = a.TotalBet + b.TotalBet, + PaidOut = a.PaidOut + b.PaidOut, + MaxBet = Math.Max(a.MaxBet, b.MaxBet), + MaxWin = Math.Max(a.MaxWin, b.MaxWin), + }))) { rakebacks.TryAdd(k.UserId, 0m); rakebacks[k.UserId] += x.TotalBet * GetHouseEdge(k.Game) * BASE_RAKEBACK; // bulk upsert in the future - await using var uow = _db.GetDbContext(); + await using var uow = db.GetDbContext(); await uow.GetTable() - .InsertOrUpdateAsync(() => new() - { - UserId = k.UserId, - Game = k.Game, - WinCount = x.WinCount, - LoseCount = Math.Max(0, x.LoseCount), - TotalBet = x.TotalBet, - PaidOut = x.PaidOut, - MaxBet = x.MaxBet, - MaxWin = x.MaxWin - }, - o => new() - { - WinCount = o.WinCount + x.WinCount, - LoseCount = Math.Max(0, o.LoseCount + x.LoseCount), - TotalBet = o.TotalBet + x.TotalBet, - PaidOut = o.PaidOut + x.PaidOut, - MaxBet = Math.Max(o.MaxBet, x.MaxBet), - MaxWin = Math.Max(o.MaxWin, x.MaxWin), - }, - () => new() - { - UserId = k.UserId, - Game = k.Game - }); + .InsertOrUpdateAsync(() => new() + { + UserId = k.UserId, + Game = k.Game, + WinCount = x.WinCount, + LoseCount = Math.Max(0, x.LoseCount), + TotalBet = x.TotalBet, + PaidOut = x.PaidOut, + MaxBet = x.MaxBet, + MaxWin = x.MaxWin + }, + o => new() + { + WinCount = o.WinCount + x.WinCount, + LoseCount = Math.Max(0, o.LoseCount + x.LoseCount), + TotalBet = o.TotalBet + x.TotalBet, + PaidOut = o.PaidOut + x.PaidOut, + MaxBet = Math.Max(o.MaxBet, x.MaxBet), + MaxWin = Math.Max(o.MaxWin, x.MaxWin), + }, + () => new() + { + UserId = k.UserId, + Game = k.Game + }); } foreach (var (k, v) in rakebacks) { - await _db.GetDbContext() - .GetTable() - .InsertOrUpdateAsync(() => new() - { - UserId = k, - Amount = v - }, - (old) => new() - { - Amount = old.Amount + v - }, - () => new() - { - UserId = k - }); + await db.GetDbContext() + .GetTable() + .InsertOrUpdateAsync(() => new() + { + UserId = k, + Amount = v + }, + (old) => new() + { + Amount = old.Amount + v + }, + () => new() + { + UserId = k + }); } } catch (Exception ex) @@ -173,10 +167,10 @@ public sealed class GamblingTxTracker : ITxTracker, IEService, IReadyExecutor private const decimal BASE_RAKEBACK = 0.05m; - public Task TrackAdd(ulong userId, long amount, TxData? txData) + public async Task TrackAdd(ulong userId, long amount, TxData? txData) { if (txData is null) - return Task.CompletedTask; + return; if (_gamblingTypes.Contains(txData.Type)) { @@ -188,7 +182,7 @@ public sealed class GamblingTxTracker : ITxTracker, IEService, IReadyExecutor var mType = GetGameType(txData.Type); if (mType is not { } type) - return Task.CompletedTask; + return; // var bigWin = _gcs.Data.BigWin; // if (bigWin > 0 && amount >= bigWin) @@ -211,7 +205,7 @@ public sealed class GamblingTxTracker : ITxTracker, IEService, IReadyExecutor MaxBet = 0, MaxWin = amount, }); - return Task.CompletedTask; + return; } } else if (txData.Type == "animalrace") @@ -230,7 +224,7 @@ public sealed class GamblingTxTracker : ITxTracker, IEService, IReadyExecutor MaxWin = 0, }); - return Task.CompletedTask; + return; } } @@ -245,14 +239,12 @@ public sealed class GamblingTxTracker : ITxTracker, IEService, IReadyExecutor MaxBet = 0, MaxWin = amount, }); - - return Task.CompletedTask; } - public Task TrackRemove(ulong userId, long amount, TxData? txData) + public async Task TrackRemove(ulong userId, long amount, TxData? txData) { if (txData is null) - return Task.CompletedTask; + return; if (_gamblingTypes.Contains(txData.Type)) { @@ -264,7 +256,7 @@ public sealed class GamblingTxTracker : ITxTracker, IEService, IReadyExecutor var mType = GetGameType(txData.Type); if (mType is not { } type) - return Task.CompletedTask; + return; userStats.Add(new UserBetStats() { @@ -278,7 +270,14 @@ public sealed class GamblingTxTracker : ITxTracker, IEService, IReadyExecutor MaxWin = 0 }); - return Task.CompletedTask; + await quests.ReportActionAsync(userId, + QuestEventType.BetPlaced, + new() + { + { "type", txData.Type }, + { "amount", amount.ToString() } + } + ); } private static GamblingGame? GetGameType(string game) @@ -296,26 +295,26 @@ public sealed class GamblingTxTracker : ITxTracker, IEService, IReadyExecutor public async Task> GetAllAsync() { - await using var ctx = _db.GetDbContext(); + await using var ctx = db.GetDbContext(); return await ctx.Set() - .ToListAsyncEF(); + .ToListAsyncEF(); } public async Task> GetUserStatsAsync(ulong userId, GamblingGame? game = null) { - await using var ctx = _db.GetDbContext(); + await using var ctx = db.GetDbContext(); if (game is null) return await ctx - .GetTable() - .Where(x => x.UserId == userId) - .ToListAsync(); + .GetTable() + .Where(x => x.UserId == userId) + .ToListAsync(); return await ctx - .GetTable() - .Where(x => x.UserId == userId && x.Game == game) - .ToListAsync(); + .GetTable() + .Where(x => x.UserId == userId && x.Game == game) + .ToListAsync(); } public decimal GetHouseEdge(GamblingGame game) @@ -330,8 +329,6 @@ public sealed class GamblingTxTracker : ITxTracker, IEService, IReadyExecutor GamblingGame.Race => 0.06m, _ => 0 }; - - } public sealed class UserBetStats diff --git a/src/EllieBot/strings/aliases.yml b/src/EllieBot/strings/aliases.yml index e09edb7..6d96439 100644 --- a/src/EllieBot/strings/aliases.yml +++ b/src/EllieBot/strings/aliases.yml @@ -374,7 +374,6 @@ quoteadd: - qa - qadd - quadd - - . quoteedit: - quoteedit - qe @@ -384,7 +383,6 @@ quoteprint: - quoteprint - qp - qup - - .. - qprint quoteshow: - quoteshow @@ -1658,4 +1656,8 @@ votefeed: vote: - vote massping: - - massping \ No newline at end of file + - massping +questlog: + - questlog + - qlog + - myquests \ No newline at end of file diff --git a/src/EllieBot/strings/commands/commands.en-US.yml b/src/EllieBot/strings/commands/commands.en-US.yml index 4fd19f1..50c5a0e 100644 --- a/src/EllieBot/strings/commands/commands.en-US.yml +++ b/src/EllieBot/strings/commands/commands.en-US.yml @@ -5206,5 +5206,12 @@ massping: Run again to cancel. ex: - '' + params: + - { } +questlog: + desc: |- + Shows your active quests and progress. + ex: + - '' params: - { } \ No newline at end of file diff --git a/src/EllieBot/strings/responses/responses.en-US.json b/src/EllieBot/strings/responses/responses.en-US.json index 77beb77..e2a233c 100644 --- a/src/EllieBot/strings/responses/responses.en-US.json +++ b/src/EllieBot/strings/responses/responses.en-US.json @@ -1244,5 +1244,9 @@ "notify_cant_set": "This event doesn't support origin channel, Please specify a channel", "vote_reward": "Thank you for voting! You've received {0}.", "vote_suggest": "Voting for the bot once every 6 hours will get you {0}!", - "vote_disabled": "Voting is disabled." + "vote_disabled": "Voting is disabled.", + "quest_log": "Quest Log", + "dailies_done": "You've completed your dailies!", + "dailies_reset": "Reset {0}", + "daily_completed": "You've completed a daily quest: {0}" } \ No newline at end of file