diff --git a/src/NadekoBot/Db/Extensions/UserXpExtensions.cs b/src/NadekoBot/Db/Extensions/UserXpExtensions.cs index 6eac5afb6..1e24309e7 100644 --- a/src/NadekoBot/Db/Extensions/UserXpExtensions.cs +++ b/src/NadekoBot/Db/Extensions/UserXpExtensions.cs @@ -31,17 +31,17 @@ public static class UserXpExtensions public static async Task> GetTopUserXps(this DbSet xps, ulong guildId, int count) => await xps.ToLinqToDBTable() .Where(x => x.GuildId == guildId) - .OrderByDescending(x => x.Xp + x.AwardedXp) + .OrderByDescending(x => x.Xp) .Take(count) .ToListAsyncLinqToDB(); public static async Task GetUserGuildRanking(this DbSet xps, ulong userId, ulong guildId) => await xps.ToLinqToDBTable() .Where(x => x.GuildId == guildId - && x.Xp + x.AwardedXp + && x.Xp > xps.AsQueryable() .Where(y => y.UserId == userId && y.GuildId == guildId) - .Select(y => y.Xp + y.AwardedXp) + .Select(y => y.Xp) .FirstOrDefault()) .CountAsyncLinqToDB() + 1; @@ -53,6 +53,6 @@ public static class UserXpExtensions => await userXp .Where(x => x.GuildId == guildId && x.UserId == userId) .FirstOrDefaultAsyncLinqToDB() is UserXpStats uxs - ? new(uxs.Xp + uxs.AwardedXp) + ? new(uxs.Xp) : new(0); } \ No newline at end of file diff --git a/src/NadekoBot/Db/LevelStats.cs b/src/NadekoBot/Db/LevelStats.cs index c6cb21f8c..79c4dc975 100644 --- a/src/NadekoBot/Db/LevelStats.cs +++ b/src/NadekoBot/Db/LevelStats.cs @@ -4,38 +4,28 @@ namespace NadekoBot.Db; public readonly struct LevelStats { - public const int XP_REQUIRED_LVL_1 = 36; - public long Level { get; } public long LevelXp { get; } public long RequiredXp { get; } public long TotalXp { get; } - public LevelStats(long xp) + public LevelStats(long totalXp) { - if (xp < 0) - xp = 0; + if (totalXp < 0) + totalXp = 0; - TotalXp = xp; - - const int baseXp = XP_REQUIRED_LVL_1; - - var required = baseXp; - var totalXp = 0; - var lvl = 1; - while (true) - { - required = (int)(baseXp + (baseXp / 4.0 * (lvl - 1))); - - if (required + totalXp > xp) - break; - - totalXp += required; - lvl++; - } - - Level = lvl - 1; - LevelXp = xp - totalXp; - RequiredXp = required; + TotalXp = totalXp; + Level = GetLevelByTotalXp(totalXp); + LevelXp = totalXp - GetTotalXpReqForLevel(Level); + RequiredXp = (9 * (Level + 1)) + 27; } + + public static LevelStats CreateForLevel(long level) + => new(GetTotalXpReqForLevel(level)); + + public static long GetTotalXpReqForLevel(long level) + => ((9 * level * level) + (63 * level)) / 2; + + public static long GetLevelByTotalXp(long totalXp) + => (long)((-7.0 / 2) + (1 / 6.0 * Math.Sqrt((8 * totalXp) + 441))); } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Administration/Self/SelfCommands.cs b/src/NadekoBot/Modules/Administration/Self/SelfCommands.cs index 95dbc5866..143f2f34a 100644 --- a/src/NadekoBot/Modules/Administration/Self/SelfCommands.cs +++ b/src/NadekoBot/Modules/Administration/Self/SelfCommands.cs @@ -66,9 +66,9 @@ public partial class Administration await message.ModifyAsync(x => x.Embed = CreateEmbed() - .WithDescription(GetText(strs.cache_users_done(added, updated))) - .WithOkColor() - .Build() + .WithDescription(GetText(strs.cache_users_done(added, updated))) + .WithOkColor() + .Build() ); } @@ -119,13 +119,13 @@ public partial class Administration await Response() .Embed(CreateEmbed() - .WithOkColor() - .WithTitle(GetText(strs.scadd)) - .AddField(GetText(strs.server), - cmd.GuildId is null ? "-" : $"{cmd.GuildName}/{cmd.GuildId}", - true) - .AddField(GetText(strs.channel), $"{cmd.ChannelName}/{cmd.ChannelId}", true) - .AddField(GetText(strs.command_text), cmdText)) + .WithOkColor() + .WithTitle(GetText(strs.scadd)) + .AddField(GetText(strs.server), + cmd.GuildId is null ? "-" : $"{cmd.GuildName}/{cmd.GuildId}", + true) + .AddField(GetText(strs.channel), $"{cmd.ChannelName}/{cmd.ChannelId}", true) + .AddField(GetText(strs.command_text), cmdText)) .SendAsync(); } @@ -502,7 +502,7 @@ public partial class Administration await _bas.SetActivityAsync(game is null ? game : await repSvc.ReplaceAsync(game, repCtx), type); await Response().Confirm(strs.set_activity).SendAsync(); - } + } [Cmd] [OwnerOnly] diff --git a/src/NadekoBot/Modules/Utility/GuildColorsCommands.cs b/src/NadekoBot/Modules/Utility/GuildColorsCommands.cs index 4475ffbe4..ace34789d 100644 --- a/src/NadekoBot/Modules/Utility/GuildColorsCommands.cs +++ b/src/NadekoBot/Modules/Utility/GuildColorsCommands.cs @@ -12,17 +12,21 @@ public partial class Utility [RequireContext(ContextType.Guild)] public async Task ServerColorsShow() { + var colors = _service.GetColors(ctx.Guild.Id); + var okHex = colors?.Ok?.RawValue.ToString("X8"); + var warnHex = colors?.Warn?.RawValue.ToString("X8"); + var errHex = colors?.Error?.RawValue.ToString("X8"); EmbedBuilder[] ebs = [ CreateEmbed() .WithOkColor() - .WithDescription("\\✅"), + .WithDescription($"\\✅ {okHex}"), CreateEmbed() .WithPendingColor() - .WithDescription("\\⏳\\⚠️"), + .WithDescription($"\\⏳\\⚠️ {warnHex}"), CreateEmbed() .WithErrorColor() - .WithDescription("\\❌") + .WithDescription($"\\❌ {errHex}") ]; await Response() diff --git a/src/NadekoBot/Modules/Xp/Xp.cs b/src/NadekoBot/Modules/Xp/Xp.cs index 1d53e186e..93b9f0a4e 100644 --- a/src/NadekoBot/Modules/Xp/Xp.cs +++ b/src/NadekoBot/Modules/Xp/Xp.cs @@ -59,9 +59,9 @@ public partial class Xp : NadekoModule var serverSetting = _service.GetNotificationType(ctx.User.Id, ctx.Guild.Id); var embed = CreateEmbed() - .WithOkColor() - .AddField(GetText(strs.xpn_setting_global), GetNotifLocationString(globalSetting)) - .AddField(GetText(strs.xpn_setting_server), GetNotifLocationString(serverSetting)); + .WithOkColor() + .AddField(GetText(strs.xpn_setting_global), GetNotifLocationString(globalSetting)) + .AddField(GetText(strs.xpn_setting_server), GetNotifLocationString(serverSetting)); await Response().Embed(embed).SendAsync(); } @@ -154,9 +154,9 @@ public partial class Xp : NadekoModule .Page((items, _) => { var embed = CreateEmbed() - .WithTitle(GetText(strs.exclusion_list)) - .WithDescription(string.Join('\n', items)) - .WithOkColor(); + .WithTitle(GetText(strs.exclusion_list)) + .WithDescription(string.Join('\n', items)) + .WithOkColor(); return embed; }) @@ -214,16 +214,12 @@ public partial class Xp : NadekoModule for (var i = 0; i < users.Count; i++) { - var levelStats = new LevelStats(users[i].Xp + users[i].AwardedXp); + var levelStats = new LevelStats(users[i].Xp); var user = ((SocketGuild)ctx.Guild).GetUser(users[i].UserId); var userXpData = users[i]; var awardStr = string.Empty; - if (userXpData.AwardedXp > 0) - awardStr = $"(+{userXpData.AwardedXp})"; - else if (userXpData.AwardedXp < 0) - awardStr = $"({userXpData.AwardedXp})"; embed.AddField($"#{i + 1 + (curPage * 10)} {user?.ToString() ?? users[i].UserId.ToString()}", $"{GetText(strs.level_x(levelStats.Level))} - {levelStats.TotalXp}xp {awardStr}"); @@ -266,8 +262,8 @@ public partial class Xp : NadekoModule .Page((users, curPage) => { var embed = CreateEmbed() - .WithOkColor() - .WithTitle(GetText(strs.global_leaderboard)); + .WithOkColor() + .WithTitle(GetText(strs.global_leaderboard)); if (!users.Any()) { @@ -287,6 +283,28 @@ public partial class Xp : NadekoModule .SendAsync(); } + [Cmd] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + [Priority(1)] + public Task XpLevelSet(int level, IGuildUser user) + => XpLevelSet(level, user.Id); + + [Cmd] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + [Priority(0)] + public async Task XpLevelSet(int level, ulong userId) + { + if (level < 0) + return; + + await _service.SetLevelAsync(ctx.Guild.Id, userId, level); + await Response() + .Confirm(strs.level_set($"<@{userId}>", Format.Bold(level.ToString()))) + .SendAsync(); + } + [Cmd] [RequireContext(ContextType.Guild)] [UserPerm(GuildPerm.Administrator)] @@ -351,8 +369,8 @@ public partial class Xp : NadekoModule public async Task XpReset(ulong userId) { var embed = CreateEmbed() - .WithTitle(GetText(strs.reset)) - .WithDescription(GetText(strs.reset_user_confirm)); + .WithTitle(GetText(strs.reset)) + .WithDescription(GetText(strs.reset_user_confirm)); if (!await PromptUserConfirmAsync(embed)) return; @@ -368,8 +386,8 @@ public partial class Xp : NadekoModule public async Task XpReset() { var embed = CreateEmbed() - .WithTitle(GetText(strs.reset)) - .WithDescription(GetText(strs.reset_server_confirm)); + .WithTitle(GetText(strs.reset)) + .WithDescription(GetText(strs.reset_server_confirm)); if (!await PromptUserConfirmAsync(embed)) return; @@ -446,20 +464,20 @@ public partial class Xp : NadekoModule { if (!items.Any()) return CreateEmbed() - .WithDescription(GetText(strs.not_found)) - .WithErrorColor(); + .WithDescription(GetText(strs.not_found)) + .WithErrorColor(); var (key, item) = items.FirstOrDefault(); var eb = CreateEmbed() - .WithOkColor() - .WithTitle(item.Name) - .AddField(GetText(strs.price), - CurrencyHelper.N(item.Price, Culture, _gss.GetCurrencySign()), - true) - .WithImageUrl(string.IsNullOrWhiteSpace(item.Preview) - ? item.Url - : item.Preview); + .WithOkColor() + .WithTitle(item.Name) + .AddField(GetText(strs.price), + CurrencyHelper.N(item.Price, Culture, _gss.GetCurrencySign()), + true) + .WithImageUrl(string.IsNullOrWhiteSpace(item.Preview) + ? item.Url + : item.Preview); if (!string.IsNullOrWhiteSpace(item.Desc)) eb.AddField(GetText(strs.desc), item.Desc); diff --git a/src/NadekoBot/Modules/Xp/XpService.cs b/src/NadekoBot/Modules/Xp/XpService.cs index 7f5683420..2df105b89 100644 --- a/src/NadekoBot/Modules/Xp/XpService.cs +++ b/src/NadekoBot/Modules/Xp/XpService.cs @@ -261,7 +261,6 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand GuildId = guildId, Xp = group.Key, DateAdded = DateTime.UtcNow, - AwardedXp = 0, NotifyOnLevelUp = XpNotificationLocation.None }, _ => new() @@ -310,8 +309,8 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand if (guildToAdd.TryGetValue(du.GuildId, out var users) && users.TryGetValue(du.UserId, out var xpGainData)) { - var oldLevel = new LevelStats(du.Xp - xpGainData.XpAmount + du.AwardedXp); - var newLevel = new LevelStats(du.Xp + du.AwardedXp); + var oldLevel = new LevelStats(du.Xp - xpGainData.XpAmount); + var newLevel = new LevelStats(du.Xp); if (oldLevel.Level < newLevel.Level) { @@ -595,7 +594,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand return await uow .UserXpStats .Where(x => x.GuildId == guildId) - .OrderByDescending(x => x.Xp + x.AwardedXp) + .OrderByDescending(x => x.Xp) .Skip(page * 10) .Take(10) .ToArrayAsyncLinqToDB(); @@ -606,7 +605,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand await using var uow = _db.GetDbContext(); return await uow.Set() .Where(x => x.GuildId == guildId && x.UserId.In(users)) - .OrderByDescending(x => x.Xp + x.AwardedXp) + .OrderByDescending(x => x.Xp) .Skip(page * 10) .Take(10) .ToArrayAsyncLinqToDB(); @@ -903,7 +902,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand using var uow = _db.GetDbContext(); var usr = uow.GetOrCreateUserXpStats(guildId, userId); - usr.AwardedXp += amount; + usr.Xp += amount; uow.SaveChanges(); } @@ -949,7 +948,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand return new(du, stats, new(totalXp), - new(stats.Xp + stats.AwardedXp), + new(stats.Xp), globalRank, guildRank); } @@ -1192,19 +1191,6 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand outlinePen)); } - if (stats.FullGuildStats.AwardedXp != 0 && template.User.Xp.Awarded.Show) - { - var sign = stats.FullGuildStats.AwardedXp > 0 ? "+ " : ""; - var awX = template.User.Xp.Awarded.Pos.X - - (Math.Max(0, stats.FullGuildStats.AwardedXp.ToString().Length - 2) * 5); - var awY = template.User.Xp.Awarded.Pos.Y; - img.Mutate(x => x.DrawText($"({sign}{stats.FullGuildStats.AwardedXp})", - _fonts.NotoSans.CreateFont(template.User.Xp.Awarded.FontSize, FontStyle.Bold), - Brushes.Solid(template.User.Xp.Awarded.Color), - outlinePen, - new(awX, awY))); - } - var rankPen = new SolidPen(Color.White, 1); //ranking if (template.User.GlobalRank.Show) @@ -1671,6 +1657,29 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand && (guildUsers == null || guildUsers.Contains(x.UserId))) .CountAsyncLinqToDB(); } + + public async Task SetLevelAsync(ulong guildId, ulong userId, int level) + { + var lvlStats = LevelStats.CreateForLevel(level); + await using var ctx = _db.GetDbContext(); + await ctx.GetTable() + .InsertOrUpdateAsync(() => new() + { + GuildId = guildId, + UserId = userId, + AwardedXp = 0, + Xp = lvlStats.TotalXp, + NotifyOnLevelUp = XpNotificationLocation.None, + DateAdded = DateTime.UtcNow + }, (old) => new() + { + Xp = lvlStats.TotalXp + }, () => new() + { + GuildId = guildId, + UserId = userId + }); + } } public enum BuyResult diff --git a/src/NadekoBot/_common/Sender/MessageSenderService.cs b/src/NadekoBot/_common/Sender/MessageSenderService.cs index caaccbb3f..3c5d11710 100644 --- a/src/NadekoBot/_common/Sender/MessageSenderService.cs +++ b/src/NadekoBot/_common/Sender/MessageSenderService.cs @@ -51,7 +51,7 @@ public class NadekoEmbedBuilder : EmbedBuilder var bcColors = bcsData.Data.Color; _okColor = guildColors?.Ok ?? bcColors.Ok.ToDiscordColor(); _errorColor = guildColors?.Error ?? bcColors.Error.ToDiscordColor(); - _pendingColor = guildColors?.Pending ?? bcColors.Pending.ToDiscordColor(); + _pendingColor = guildColors?.Warn ?? bcColors.Pending.ToDiscordColor(); } public EmbedBuilder WithOkColor() diff --git a/src/NadekoBot/_common/Services/Impl/GuildColorsService.cs b/src/NadekoBot/_common/Services/Impl/GuildColorsService.cs index e38c028b7..1f883a60b 100644 --- a/src/NadekoBot/_common/Services/Impl/GuildColorsService.cs +++ b/src/NadekoBot/_common/Services/Impl/GuildColorsService.cs @@ -108,7 +108,7 @@ public sealed class GuildColorsService : IReadyExecutor, IGuildColorsService, IN { _colors[guildId] = _colors[guildId] with { - Pending = color?.ToDiscordColor() + Warn = color?.ToDiscordColor() }; } } diff --git a/src/NadekoBot/_common/Services/Impl/IGuildColorsService.cs b/src/NadekoBot/_common/Services/Impl/IGuildColorsService.cs index 740f69773..ddbd68fba 100644 --- a/src/NadekoBot/_common/Services/Impl/IGuildColorsService.cs +++ b/src/NadekoBot/_common/Services/Impl/IGuildColorsService.cs @@ -10,4 +10,4 @@ public interface IGuildColorsService Task SetPendingColor(ulong guildId, Rgba32? color); } -public record struct Colors(Color? Ok, Color? Pending, Color? Error); \ No newline at end of file +public record struct Colors(Color? Ok, Color? Warn, Color? Error); \ No newline at end of file diff --git a/src/NadekoBot/data/aliases.yml b/src/NadekoBot/data/aliases.yml index 5e15ebe27..5c97900a8 100644 --- a/src/NadekoBot/data/aliases.yml +++ b/src/NadekoBot/data/aliases.yml @@ -1121,6 +1121,8 @@ xpshopbuy: - xpshopbuy xpshopuse: - xpshopuse +xplevelset: + - xplevelset clubcreate: - clubcreate clubtransfer: diff --git a/src/NadekoBot/data/strings/commands/commands.en-US.yml b/src/NadekoBot/data/strings/commands/commands.en-US.yml index c8fe62fde..b76acdff6 100644 --- a/src/NadekoBot/data/strings/commands/commands.en-US.yml +++ b/src/NadekoBot/data/strings/commands/commands.en-US.yml @@ -4821,3 +4821,13 @@ servercolorpending: params: - color: desc: "The hex of the color to set" +xplevelset: + desc: |- + Sets the level of the user you specify. + ex: + - '10 @User' + params: + - level: + desc: "The level to set the user to." + - user: + desc: "The user to set the level of." \ No newline at end of file diff --git a/src/NadekoBot/data/strings/responses/responses.en-US.json b/src/NadekoBot/data/strings/responses/responses.en-US.json index 321b0478a..ffcafa129 100644 --- a/src/NadekoBot/data/strings/responses/responses.en-US.json +++ b/src/NadekoBot/data/strings/responses/responses.en-US.json @@ -1137,5 +1137,6 @@ "server_not_found": "Server not found.", "server_color_set": "Successfully set a new server color.", "lasts_until": "Lasts Until", - "winners_count": "Winners" + "winners_count": "Winners", + "level_set": "Level of user {0} set to {1} on this server." }