mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-10 17:28:27 -04:00
Compare commits
8 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
40f1774655 | ||
|
fddd0f2340 | ||
|
86f9d901fe | ||
|
eaab60898f | ||
|
e40c9335c1 | ||
|
d921b6889d | ||
|
aaef365bdc | ||
|
a01a646cbf |
41
CHANGELOG.md
41
CHANGELOG.md
@@ -2,6 +2,47 @@
|
||||
|
||||
Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o
|
||||
|
||||
## [5.3.8] - 27.01.2025
|
||||
|
||||
## Fixed
|
||||
|
||||
- `.temprole` now correctly adds a role
|
||||
- `.h temprole` also shows the correct overload now
|
||||
|
||||
## [5.3.7] - 21.01.2025
|
||||
|
||||
## Changed
|
||||
|
||||
- You can now run `.prune` in DMs
|
||||
- It deletes only bot messages
|
||||
- You can't specify a number of messages to delete (100 default)
|
||||
- Updated command list
|
||||
|
||||
## [5.3.6] - 20.01.2025
|
||||
|
||||
## Added
|
||||
|
||||
- Added player skill stat when fishing
|
||||
- Starts at 0, goes up to 100
|
||||
- Every time you fish you have a chance to get an extra skill point
|
||||
- Higher skill gives you more chance to catch fish (and therefore less chance to catch trash)
|
||||
|
||||
## Changed
|
||||
|
||||
- Patrons no longer have `.timely` and `.fish` captcha on the public bot
|
||||
|
||||
## Fixed
|
||||
|
||||
- Fixed fishing spots again (Your channels will once again change a spot, last time hopefully)
|
||||
- There was a mistake in spot calculation for each channel
|
||||
|
||||
## [5.3.5] - 17.01.2025
|
||||
|
||||
## Fixed
|
||||
|
||||
- .sar rm will now accept role ids in case the role was deleted
|
||||
- `.deletewaifus` should work again
|
||||
|
||||
## [5.3.4] - 14.01.2025
|
||||
|
||||
## Added
|
||||
|
4178
src/NadekoBot/Migrations/PostgreSql/20250118235233_fish-skill.Designer.cs
generated
Normal file
4178
src/NadekoBot/Migrations/PostgreSql/20250118235233_fish-skill.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,42 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace NadekoBot.Migrations.PostgreSql
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class fishskill : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "userfishstats",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
userid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
|
||||
skill = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_userfishstats", x => x.id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_userfishstats_userid",
|
||||
table: "userfishstats",
|
||||
column: "userid",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "userfishstats");
|
||||
}
|
||||
}
|
||||
}
|
@@ -3408,6 +3408,33 @@ namespace NadekoBot.Migrations.PostgreSql
|
||||
b.ToTable("fishcatch", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NadekoBot.Modules.Games.UserFishStats", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("Skill")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("skill");
|
||||
|
||||
b.Property<decimal>("UserId")
|
||||
.HasColumnType("numeric(20,0)")
|
||||
.HasColumnName("userid");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_userfishstats");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_userfishstats_userid");
|
||||
|
||||
b.ToTable("userfishstats", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NadekoBot.Services.GreetSettings", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
3218
src/NadekoBot/Migrations/Sqlite/20250118235223_fish-skill.Designer.cs
generated
Normal file
3218
src/NadekoBot/Migrations/Sqlite/20250118235223_fish-skill.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
41
src/NadekoBot/Migrations/Sqlite/20250118235223_fish-skill.cs
Normal file
41
src/NadekoBot/Migrations/Sqlite/20250118235223_fish-skill.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace NadekoBot.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class fishskill : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "UserFishStats",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
UserId = table.Column<ulong>(type: "INTEGER", nullable: false),
|
||||
Skill = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_UserFishStats", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UserFishStats_UserId",
|
||||
table: "UserFishStats",
|
||||
column: "UserId",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "UserFishStats");
|
||||
}
|
||||
}
|
||||
}
|
@@ -2533,6 +2533,26 @@ namespace NadekoBot.Migrations
|
||||
b.ToTable("FishCatch");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NadekoBot.Modules.Games.UserFishStats", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Skill")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<ulong>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("UserFishStats");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NadekoBot.Services.GreetSettings", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
@@ -32,6 +32,25 @@ public partial class Administration
|
||||
}
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.DM)]
|
||||
[NadekoOptions<PruneOptions>]
|
||||
public async Task Prune()
|
||||
{
|
||||
var progressMsg = await Response().Pending(strs.prune_progress(0, 100)).SendAsync();
|
||||
var progress = GetProgressTracker(progressMsg);
|
||||
|
||||
var result = await _service.PruneWhere(ctx.Channel,
|
||||
100,
|
||||
x => x.Author.Id == ctx.Client.CurrentUser.Id,
|
||||
progress);
|
||||
|
||||
ctx.Message.DeleteAfter(3);
|
||||
|
||||
await SendResult(result);
|
||||
await progressMsg.DeleteAsync();
|
||||
}
|
||||
|
||||
//deletes her own messages, no perm required
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
@@ -114,9 +133,9 @@ public partial class Administration
|
||||
await progressMsg.ModifyAsync(props =>
|
||||
{
|
||||
props.Embed = CreateEmbed()
|
||||
.WithPendingColor()
|
||||
.WithDescription(GetText(strs.prune_progress(deleted, total)))
|
||||
.Build();
|
||||
.WithPendingColor()
|
||||
.WithDescription(GetText(strs.prune_progress(deleted, total)))
|
||||
.Build();
|
||||
});
|
||||
}
|
||||
catch
|
||||
|
@@ -18,7 +18,7 @@ public class PruneService : INService
|
||||
}
|
||||
|
||||
public async Task<PruneResult> PruneWhere(
|
||||
ITextChannel channel,
|
||||
IMessageChannel channel,
|
||||
int amount,
|
||||
Func<IMessage, bool> predicate,
|
||||
IProgress<(int deleted, int total)> progress,
|
||||
@@ -30,13 +30,14 @@ public class PruneService : INService
|
||||
|
||||
var originalAmount = amount;
|
||||
|
||||
var gid = (channel as ITextChannel)?.GuildId ?? channel.Id;
|
||||
using var cancelSource = new CancellationTokenSource();
|
||||
if (!_pruningGuilds.TryAdd(channel.GuildId, cancelSource))
|
||||
if (!_pruningGuilds.TryAdd(gid, cancelSource))
|
||||
return PruneResult.AlreadyRunning;
|
||||
|
||||
try
|
||||
{
|
||||
if (!await _ps.LimitHitAsync(LimitedFeatureName.Prune, channel.Guild.OwnerId))
|
||||
if (channel is ITextChannel tc && !await _ps.LimitHitAsync(LimitedFeatureName.Prune, tc.Guild.OwnerId))
|
||||
{
|
||||
return PruneResult.FeatureLimit;
|
||||
}
|
||||
@@ -74,9 +75,9 @@ public class PruneService : INService
|
||||
singleDeletable.Add(x);
|
||||
}
|
||||
|
||||
if (bulkDeletable.Count > 0)
|
||||
if (channel is ITextChannel tc2 && bulkDeletable.Count > 0)
|
||||
{
|
||||
await channel.DeleteMessagesAsync(bulkDeletable);
|
||||
await tc2.DeleteMessagesAsync(bulkDeletable);
|
||||
amount -= msgs.Length;
|
||||
progress.Report((originalAmount - amount, originalAmount));
|
||||
await Task.Delay(2000, cancelSource.Token);
|
||||
@@ -97,7 +98,7 @@ public class PruneService : INService
|
||||
}
|
||||
finally
|
||||
{
|
||||
_pruningGuilds.TryRemove(channel.GuildId, out _);
|
||||
_pruningGuilds.TryRemove(gid, out _);
|
||||
}
|
||||
|
||||
return PruneResult.Success;
|
||||
|
@@ -221,7 +221,7 @@ public partial class Administration
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.Administrator)]
|
||||
[BotPerm(GuildPerm.ManageRoles)]
|
||||
public async Task TempRole(ParsedTimespan timespan, IUser user, [Leftover] IRole role)
|
||||
public async Task TempRole(ParsedTimespan timespan, IGuildUser user, [Leftover] IRole role)
|
||||
{
|
||||
if (!await CheckRoleHierarchy(role))
|
||||
{
|
||||
@@ -231,6 +231,7 @@ public partial class Administration
|
||||
return;
|
||||
}
|
||||
|
||||
await user.AddRoleAsync(role);
|
||||
await _tempRoleService.AddTempRoleAsync(ctx.Guild.Id, role.Id, user.Id, timespan.Time);
|
||||
|
||||
|
||||
|
@@ -192,15 +192,22 @@ public partial class Administration
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.ManageRoles)]
|
||||
public async Task SarRemove([Leftover] IRole role)
|
||||
[Priority(1)]
|
||||
public Task SarRemove([Leftover] IRole role)
|
||||
=> SarRemove(role.Id);
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.ManageRoles)]
|
||||
[Priority(0)]
|
||||
public async Task SarRemove([Leftover] ulong roleId)
|
||||
{
|
||||
var guser = (IGuildUser)ctx.User;
|
||||
|
||||
var success = await _service.RemoveAsync(role.Guild.Id, role.Id);
|
||||
var role = await ctx.Guild.GetRoleAsync(roleId);
|
||||
var success = await _service.RemoveAsync(ctx.Guild.Id, roleId);
|
||||
if (!success)
|
||||
await Response().Error(strs.self_assign_not).SendAsync();
|
||||
else
|
||||
await Response().Confirm(strs.self_assign_rem(Format.Bold(role.Name))).SendAsync();
|
||||
await Response().Confirm(strs.self_assign_rem(Format.Bold(role?.Name ?? roleId.ToString()))).SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
|
@@ -13,6 +13,7 @@ using System.Globalization;
|
||||
using System.Text;
|
||||
using NadekoBot.Modules.Gambling.Rps;
|
||||
using NadekoBot.Common.TypeReaders;
|
||||
using NadekoBot.Modules.Games;
|
||||
using NadekoBot.Modules.Patronage;
|
||||
using SixLabors.Fonts;
|
||||
using SixLabors.Fonts.Unicode;
|
||||
@@ -40,6 +41,7 @@ public partial class Gambling : GamblingModule<GamblingService>
|
||||
private readonly IPatronageService _ps;
|
||||
private readonly RakebackService _rb;
|
||||
private readonly IBotCache _cache;
|
||||
private readonly CaptchaService _captchaService;
|
||||
|
||||
public Gambling(
|
||||
IGamblingService gs,
|
||||
@@ -54,7 +56,8 @@ public partial class Gambling : GamblingModule<GamblingService>
|
||||
IPatronageService patronage,
|
||||
GamblingTxTracker gamblingTxTracker,
|
||||
RakebackService rb,
|
||||
IBotCache cache)
|
||||
IBotCache cache,
|
||||
CaptchaService captchaService)
|
||||
: base(configService)
|
||||
{
|
||||
_gs = gs;
|
||||
@@ -66,6 +69,7 @@ public partial class Gambling : GamblingModule<GamblingService>
|
||||
_gamblingTxTracker = gamblingTxTracker;
|
||||
_rb = rb;
|
||||
_cache = cache;
|
||||
_captchaService = captchaService;
|
||||
_ps = patronage;
|
||||
_rng = new NadekoRandom();
|
||||
|
||||
@@ -154,49 +158,42 @@ public partial class Gambling : GamblingModule<GamblingService>
|
||||
}
|
||||
else if (Config.Timely.ProtType == TimelyProt.Captcha)
|
||||
{
|
||||
var password = await GetUserTimelyPassword(ctx.User.Id);
|
||||
var img = GetPasswordImage(password);
|
||||
using var stream = await img.ToStreamAsync();
|
||||
var captcha = await Response()
|
||||
.File(stream, "timely.png")
|
||||
.SendAsync();
|
||||
try
|
||||
{
|
||||
var userInput = await GetUserInputAsync(ctx.User.Id, ctx.Channel.Id);
|
||||
if (userInput?.ToLowerInvariant() != password?.ToLowerInvariant())
|
||||
{
|
||||
return;
|
||||
}
|
||||
var password = await _captchaService.GetUserCaptcha(ctx.User.Id);
|
||||
|
||||
await ClearUserTimelyPassword(ctx.User.Id);
|
||||
}
|
||||
finally
|
||||
if (password is not null)
|
||||
{
|
||||
_ = captcha.DeleteAsync();
|
||||
var img = GetPasswordImage(password);
|
||||
await using var stream = await img.ToStreamAsync();
|
||||
var toSend = Response()
|
||||
.File(stream, "timely.png");
|
||||
|
||||
#if GLOBAL_NADEKO
|
||||
if (_rng.Next(0, 5) == 0)
|
||||
toSend = toSend
|
||||
.Confirm("[Sub on Patreon](https://patreon.com/nadekobot) to remove captcha.");
|
||||
#endif
|
||||
|
||||
var captchaMessage = await toSend.SendAsync();
|
||||
try
|
||||
{
|
||||
var userInput = await GetUserInputAsync(ctx.User.Id, ctx.Channel.Id);
|
||||
if (userInput?.ToLowerInvariant() != password?.ToLowerInvariant())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await _captchaService.ClearUserCaptcha(ctx.User.Id);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_ = captchaMessage.DeleteAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await ClaimTimely();
|
||||
}
|
||||
|
||||
private static TypedKey<string> TimelyPasswordKey(ulong userId)
|
||||
=> new($"timely_password:{userId}");
|
||||
|
||||
private async Task<string> GetUserTimelyPassword(ulong userId)
|
||||
{
|
||||
var pw = await _cache.GetOrAddAsync(TimelyPasswordKey(userId),
|
||||
() =>
|
||||
{
|
||||
var password = _service.GeneratePassword();
|
||||
return Task.FromResult(password);
|
||||
});
|
||||
|
||||
return pw;
|
||||
}
|
||||
|
||||
private ValueTask<bool> ClearUserTimelyPassword(ulong userId)
|
||||
=> _cache.RemoveAsync(TimelyPasswordKey(userId));
|
||||
|
||||
private Image<Rgba32> GetPasswordImage(string password)
|
||||
{
|
||||
var img = new Image<Rgba32>(50, 24);
|
||||
|
@@ -16,9 +16,9 @@ public class GamblingCleanupService : IGamblingCleanupService, INService
|
||||
public async Task DeleteWaifus()
|
||||
{
|
||||
await using var ctx = _db.GetDbContext();
|
||||
await ctx.GetTable<WaifuInfo>().DeleteAsync();
|
||||
await ctx.GetTable<WaifuItem>().DeleteAsync();
|
||||
await ctx.GetTable<WaifuUpdate>().DeleteAsync();
|
||||
await ctx.GetTable<WaifuInfo>().DeleteAsync();
|
||||
}
|
||||
|
||||
public async Task DeleteWaifu(ulong userId)
|
||||
|
@@ -1,4 +1,6 @@
|
||||
using SixLabors.Fonts;
|
||||
using NadekoBot.Db.Models;
|
||||
using NadekoBot.Modules.Patronage;
|
||||
using SixLabors.Fonts;
|
||||
using SixLabors.Fonts.Unicode;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Drawing.Processing;
|
||||
@@ -9,7 +11,7 @@ using Color = SixLabors.ImageSharp.Color;
|
||||
|
||||
namespace NadekoBot.Modules.Games;
|
||||
|
||||
public sealed class CaptchaService(FontProvider fonts) : INService
|
||||
public sealed class CaptchaService(FontProvider fonts, IBotCache cache, IPatronageService ps) : INService
|
||||
{
|
||||
private readonly NadekoRandom _rng = new();
|
||||
|
||||
@@ -53,4 +55,26 @@ public sealed class CaptchaService(FontProvider fonts) : INService
|
||||
var num = _rng.Next((int)Math.Pow(31, 2), (int)Math.Pow(32, 3));
|
||||
return new kwum(num).ToString();
|
||||
}
|
||||
|
||||
private static TypedKey<string> CaptchaPasswordKey(ulong userId)
|
||||
=> new($"timely_password:{userId}");
|
||||
|
||||
public async Task<string?> GetUserCaptcha(ulong userId)
|
||||
{
|
||||
var patron = await ps.GetPatronAsync(userId);
|
||||
if (patron is Patron p && !p.ValidThru.IsBeforeToday())
|
||||
return null;
|
||||
|
||||
var pw = await cache.GetOrAddAsync(CaptchaPasswordKey(userId),
|
||||
() =>
|
||||
{
|
||||
var password = GeneratePassword();
|
||||
return Task.FromResult(password)!;
|
||||
});
|
||||
|
||||
return pw;
|
||||
}
|
||||
|
||||
public ValueTask<bool> ClearUserCaptcha(ulong userId)
|
||||
=> cache.RemoveAsync(CaptchaPasswordKey(userId));
|
||||
}
|
@@ -1,4 +1,5 @@
|
||||
using System.Text;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text;
|
||||
using Format = Discord.Format;
|
||||
|
||||
namespace NadekoBot.Modules.Games;
|
||||
@@ -9,8 +10,10 @@ public partial class Games
|
||||
FishService fs,
|
||||
FishConfigService fcs,
|
||||
IBotCache cache,
|
||||
CaptchaService service) : NadekoModule
|
||||
CaptchaService captchaService) : NadekoModule
|
||||
{
|
||||
private static readonly NadekoRandom _rng = new();
|
||||
|
||||
private TypedKey<bool> FishingWhitelistKey(ulong userId)
|
||||
=> new($"fishingwhitelist:{userId}");
|
||||
|
||||
@@ -20,29 +23,39 @@ public partial class Games
|
||||
var cRes = await cache.GetAsync(FishingWhitelistKey(ctx.User.Id));
|
||||
if (cRes.TryPickT1(out _, out _))
|
||||
{
|
||||
var password = await GetUserCaptcha(ctx.User.Id);
|
||||
var img = service.GetPasswordImage(password);
|
||||
using var stream = await img.ToStreamAsync();
|
||||
var captcha = await Response()
|
||||
.File(stream, "timely.png")
|
||||
.SendAsync();
|
||||
|
||||
try
|
||||
var password = await captchaService.GetUserCaptcha(ctx.User.Id);
|
||||
if (password is not null)
|
||||
{
|
||||
var userInput = await GetUserInputAsync(ctx.User.Id, ctx.Channel.Id);
|
||||
if (userInput?.ToLowerInvariant() != password?.ToLowerInvariant())
|
||||
var img = captchaService.GetPasswordImage(password);
|
||||
using var stream = await img.ToStreamAsync();
|
||||
|
||||
var toSend = Response()
|
||||
.File(stream, "timely.png");
|
||||
|
||||
#if GLOBAL_NADEKO
|
||||
if (_rng.Next(0, 5) == 0)
|
||||
toSend = toSend
|
||||
.Confirm("[Sub on Patreon](https://patreon.com/nadekobot) to remove captcha.");
|
||||
#endif
|
||||
var captcha = await toSend.SendAsync();
|
||||
|
||||
try
|
||||
{
|
||||
return;
|
||||
}
|
||||
var userInput = await GetUserInputAsync(ctx.User.Id, ctx.Channel.Id);
|
||||
if (userInput?.ToLowerInvariant() != password?.ToLowerInvariant())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// whitelist the user for 30 minutes
|
||||
await cache.AddAsync(FishingWhitelistKey(ctx.User.Id), true, TimeSpan.FromMinutes(30));
|
||||
// reset the password
|
||||
await ClearUserCaptcha(ctx.User.Id);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_ = captcha.DeleteAsync();
|
||||
// whitelist the user for 30 minutes
|
||||
await cache.AddAsync(FishingWhitelistKey(ctx.User.Id), true, TimeSpan.FromMinutes(30));
|
||||
// reset the password
|
||||
await captchaService.ClearUserCaptcha(ctx.User.Id);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_ = captcha.DeleteAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,12 +89,18 @@ public partial class Games
|
||||
return;
|
||||
}
|
||||
|
||||
var desc = GetText(strs.fish_caught(res.Fish.Emoji + " " + Format.Bold(res.Fish.Name)));
|
||||
|
||||
if (res.IsSkillUp)
|
||||
{
|
||||
desc += "\n" + GetText(strs.fish_skill_up(res.Skill, res.MaxSkill));
|
||||
}
|
||||
|
||||
await Response()
|
||||
.Embed(CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithAuthor(ctx.User)
|
||||
.WithDescription(GetText(strs.fish_caught(Format.Bold(res.Fish.Name))))
|
||||
.WithDescription(desc)
|
||||
.AddField(GetText(strs.fish_quality), GetStarText(res.Stars, res.Fish.Stars), true)
|
||||
.AddField(GetText(strs.desc), res.Fish.Fluff, true)
|
||||
.WithThumbnailUrl(res.Fish.Image))
|
||||
@@ -117,8 +136,8 @@ public partial class Games
|
||||
|
||||
var fishes = await fs.GetAllFish();
|
||||
|
||||
Log.Information(fishes.Count.ToString());
|
||||
var catches = await fs.GetUserCatches(ctx.User.Id);
|
||||
var (skill, maxSkill) = await fs.GetSkill(ctx.User.Id);
|
||||
|
||||
var catchDict = catches.ToDictionary(x => x.FishId, x => x);
|
||||
|
||||
@@ -130,7 +149,10 @@ public partial class Games
|
||||
.Page((fs, i) =>
|
||||
{
|
||||
var eb = CreateEmbed()
|
||||
.WithOkColor();
|
||||
.WithDescription($"🧠 **Skill:** {skill} / {maxSkill}")
|
||||
.WithAuthor(ctx.User)
|
||||
.WithTitle(GetText(strs.fish_list_title))
|
||||
.WithOkColor();
|
||||
|
||||
foreach (var f in fs)
|
||||
{
|
||||
@@ -224,48 +246,9 @@ public partial class Games
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static TypedKey<string> CaptchaPasswordKey(ulong userId)
|
||||
=> new($"timely_password:{userId}");
|
||||
|
||||
private async Task<string> GetUserCaptcha(ulong userId)
|
||||
{
|
||||
var pw = await cache.GetOrAddAsync(CaptchaPasswordKey(userId),
|
||||
() =>
|
||||
{
|
||||
var password = service.GeneratePassword();
|
||||
return Task.FromResult(password)!;
|
||||
});
|
||||
|
||||
return pw!;
|
||||
}
|
||||
|
||||
private ValueTask<bool> ClearUserCaptcha(ulong userId)
|
||||
=> cache.RemoveAsync(CaptchaPasswordKey(userId));
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// public sealed class UserFishStats
|
||||
// {
|
||||
// [Key]
|
||||
// public int Id { get; set; }
|
||||
//
|
||||
// public ulong UserId { get; set; }
|
||||
//
|
||||
// public ulong CommonCatches { get; set; }
|
||||
// public ulong RareCatches { get; set; }
|
||||
// public ulong VeryRareCatches { get; set; }
|
||||
// public ulong EpicCatches { get; set; }
|
||||
//
|
||||
// public ulong CommonMaxCatches { get; set; }
|
||||
// public ulong RareMaxCatches { get; set; }
|
||||
// public ulong VeryRareMaxCatches { get; set; }
|
||||
// public ulong EpicMaxCatches { get; set; }
|
||||
//
|
||||
// public int TotalStars { get; set; }
|
||||
// }
|
||||
|
||||
public enum FishingSpot
|
||||
{
|
||||
Ocean,
|
||||
|
@@ -4,6 +4,9 @@ public sealed class FishResult
|
||||
{
|
||||
public required FishData Fish { get; init; }
|
||||
public int Stars { get; init; }
|
||||
public bool IsSkillUp { get; set; }
|
||||
public int Skill { get; set; }
|
||||
public int MaxSkill { get; set; }
|
||||
}
|
||||
|
||||
public readonly record struct AlreadyFishing;
|
||||
|
||||
|
@@ -1,11 +1,16 @@
|
||||
using LinqToDB;
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace NadekoBot.Modules.Games;
|
||||
|
||||
public sealed class FishService(FishConfigService fcs, IBotCache cache, DbService db) : INService
|
||||
{
|
||||
public const double MAX_SKILL = 100;
|
||||
|
||||
private Random _rng = new Random();
|
||||
|
||||
private static TypedKey<bool> FishingKey(ulong userId)
|
||||
@@ -28,20 +33,104 @@ public sealed class FishService(FishConfigService fcs, IBotCache cache, DbServic
|
||||
var conf = fcs.Data;
|
||||
await Task.Delay(TimeSpan.FromSeconds(duration));
|
||||
|
||||
var (playerSkill, _) = await GetSkill(userId);
|
||||
var fishChanceMultiplier = Math.Clamp((playerSkill + 20) / MAX_SKILL, 0, 1);
|
||||
var trashChanceMultiplier = Math.Clamp(((2 * MAX_SKILL) - playerSkill) / MAX_SKILL, 1, 2);
|
||||
|
||||
var nothingChance = conf.Chance.Nothing;
|
||||
var fishChance = conf.Chance.Fish * fishChanceMultiplier;
|
||||
var trashChance = conf.Chance.Trash * trashChanceMultiplier;
|
||||
|
||||
// first roll whether it's fish, trash or nothing
|
||||
var totalChance = conf.Chance.Fish + conf.Chance.Trash + conf.Chance.Nothing;
|
||||
var totalChance = fishChance + trashChance + conf.Chance.Nothing;
|
||||
|
||||
var typeRoll = _rng.NextDouble() * totalChance;
|
||||
|
||||
if (typeRoll < conf.Chance.Nothing)
|
||||
if (typeRoll < nothingChance)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var items = typeRoll < conf.Chance.Nothing + conf.Chance.Fish
|
||||
var items = typeRoll < nothingChance + fishChance
|
||||
? conf.Fish
|
||||
: conf.Trash;
|
||||
|
||||
return await FishAsyncInternal(userId, channelId, items);
|
||||
|
||||
var result = await FishAsyncInternal(userId, channelId, items);
|
||||
|
||||
if (result is not null)
|
||||
{
|
||||
var isSkillUp = await TrySkillUpAsync(userId, playerSkill);
|
||||
|
||||
result.IsSkillUp = isSkillUp;
|
||||
result.MaxSkill = (int)MAX_SKILL;
|
||||
result.Skill = playerSkill;
|
||||
|
||||
if (isSkillUp)
|
||||
{
|
||||
result.Skill += 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task<bool> TrySkillUpAsync(ulong userId, int playerSkill)
|
||||
{
|
||||
var skillUpProb = GetSkillUpProb(playerSkill);
|
||||
|
||||
var rng = _rng.NextDouble();
|
||||
|
||||
if (rng < skillUpProb)
|
||||
{
|
||||
await using var ctx = db.GetDbContext();
|
||||
|
||||
var maxSkill = (int)MAX_SKILL;
|
||||
await ctx.GetTable<UserFishStats>()
|
||||
.InsertOrUpdateAsync(() => new()
|
||||
{
|
||||
UserId = userId,
|
||||
Skill = 1,
|
||||
},
|
||||
(old) => new()
|
||||
{
|
||||
UserId = userId,
|
||||
Skill = old.Skill > maxSkill ? maxSkill : old.Skill + 1
|
||||
},
|
||||
() => new()
|
||||
{
|
||||
UserId = userId,
|
||||
Skill = playerSkill
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private double GetSkillUpProb(int playerSkill)
|
||||
{
|
||||
if (playerSkill < 0)
|
||||
playerSkill = 0;
|
||||
|
||||
if (playerSkill >= 100)
|
||||
return 0;
|
||||
|
||||
return 1 / (Math.Pow(Math.E, playerSkill / 22d));
|
||||
}
|
||||
|
||||
public async Task<(int skill, int maxSkill)> GetSkill(ulong userId)
|
||||
{
|
||||
await using var ctx = db.GetDbContext();
|
||||
|
||||
var skill = await ctx.GetTable<UserFishStats>()
|
||||
.Where(x => x.UserId == userId)
|
||||
.Select(x => x.Skill)
|
||||
.FirstOrDefaultAsyncLinqToDB();
|
||||
|
||||
return (skill, (int)MAX_SKILL);
|
||||
}
|
||||
|
||||
private async Task<FishResult?> FishAsyncInternal(ulong userId, ulong channelId, List<FishData> items)
|
||||
@@ -133,7 +222,7 @@ public sealed class FishService(FishConfigService fcs, IBotCache cache, DbServic
|
||||
|
||||
public FishingSpot GetSpot(ulong channelId)
|
||||
{
|
||||
var cid = (channelId >> 22 >> 8) % 10;
|
||||
var cid = (channelId >> 22 >> 29) % 10;
|
||||
|
||||
return cid switch
|
||||
{
|
||||
@@ -159,7 +248,6 @@ public sealed class FishService(FishConfigService fcs, IBotCache cache, DbServic
|
||||
return FishingTime.Day;
|
||||
|
||||
return FishingTime.Dusk;
|
||||
|
||||
}
|
||||
|
||||
private const int WEATHER_PERIODS_PER_DAY = 12;
|
||||
@@ -297,7 +385,7 @@ public sealed class FishService(FishConfigService fcs, IBotCache cache, DbServic
|
||||
public async Task<List<FishData>> GetAllFish()
|
||||
{
|
||||
await Task.Yield();
|
||||
|
||||
|
||||
var conf = fcs.Data;
|
||||
return conf.Fish.Concat(conf.Trash).ToList();
|
||||
}
|
||||
@@ -312,4 +400,22 @@ public sealed class FishService(FishConfigService fcs, IBotCache cache, DbServic
|
||||
|
||||
return catches;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class UserFishStats
|
||||
{
|
||||
[Key]
|
||||
public int Id { get; set; }
|
||||
|
||||
public ulong UserId { get; set; }
|
||||
public int Skill { get; set; }
|
||||
}
|
||||
|
||||
public sealed class UserFishStatsConfiguration : IEntityTypeConfiguration<UserFishStats>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<UserFishStats> builder)
|
||||
{
|
||||
builder.HasIndex(x => x.UserId)
|
||||
.IsUnique();
|
||||
}
|
||||
}
|
@@ -4,7 +4,7 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>true</ImplicitUsings>
|
||||
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||
<Version>5.3.4</Version>
|
||||
<Version>5.3.8</Version>
|
||||
|
||||
<!-- Output/build -->
|
||||
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>
|
||||
|
@@ -974,7 +974,7 @@
|
||||
"Module": "Administration",
|
||||
"Options": null,
|
||||
"Requirements": [
|
||||
"Bot Owner Only"
|
||||
"Administrator Server Permission"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -990,7 +990,7 @@
|
||||
"Module": "Administration",
|
||||
"Options": null,
|
||||
"Requirements": [
|
||||
"Bot Owner Only"
|
||||
"Administrator Server Permission"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1008,7 +1008,7 @@
|
||||
"Module": "Administration",
|
||||
"Options": null,
|
||||
"Requirements": [
|
||||
"Bot Owner Only"
|
||||
"Administrator Server Permission"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -4045,6 +4045,51 @@
|
||||
"ManageMessages Server Permission"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".fish",
|
||||
".fi"
|
||||
],
|
||||
"Description": "Attempt to catch a fish.\nDifferent fish live in different places, at different times of day and in different weather.",
|
||||
"Usage": [
|
||||
".fish"
|
||||
],
|
||||
"Submodule": "FishCommands",
|
||||
"Module": "Games",
|
||||
"Options": null,
|
||||
"Requirements": []
|
||||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".fishspot",
|
||||
".fisp",
|
||||
".fish?"
|
||||
],
|
||||
"Description": "Shows information about the current fish spot, weather and time.",
|
||||
"Usage": [
|
||||
".fishspot"
|
||||
],
|
||||
"Submodule": "FishCommands",
|
||||
"Module": "Games",
|
||||
"Options": null,
|
||||
"Requirements": []
|
||||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".fishlist",
|
||||
".fili",
|
||||
".fishes",
|
||||
".fil"
|
||||
],
|
||||
"Description": "Look at your fish catalogue.\nShows how many of each fish you caught and what was the highest quality.\nFor each caught fish, it also shows its required spot, time of day and weather.",
|
||||
"Usage": [
|
||||
".fishlist"
|
||||
],
|
||||
"Submodule": "FishCommands",
|
||||
"Module": "Games",
|
||||
"Options": null,
|
||||
"Requirements": []
|
||||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".hangmanlist"
|
||||
|
@@ -4852,11 +4852,11 @@ temprole:
|
||||
- '15m @User Jail'
|
||||
- '7d @Newbie Trial Member'
|
||||
params:
|
||||
- days:
|
||||
- time:
|
||||
desc: "The time after which the role is automatically removed."
|
||||
- user:
|
||||
user:
|
||||
desc: "The user to give the role to."
|
||||
- role:
|
||||
role:
|
||||
desc: "The role to give to the user."
|
||||
minesweeper:
|
||||
desc: |-
|
||||
|
@@ -1169,5 +1169,7 @@
|
||||
"fish_weather_duration": "Each weather period lasts for {0} hours.",
|
||||
"fish_weather_current": "Current",
|
||||
"fish_weather_forecast": "Forecast",
|
||||
"fish_tod": "Time of Day"
|
||||
"fish_tod": "Time of Day",
|
||||
"fish_skill_up": "Fishing skill increased to **{0} / {1}**",
|
||||
"fish_list_title": "Fishing"
|
||||
}
|
||||
|
Reference in New Issue
Block a user