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