mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-10 09:18:27 -04:00
add: Added fishing skill stat
fix: fixed fishing spot calculation change: patrons no longer have captchas on the public bot docs: Upped version to 5.3.6
This commit is contained in:
18
CHANGELOG.md
18
CHANGELOG.md
@@ -2,6 +2,24 @@
|
|||||||
|
|
||||||
Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o
|
Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o
|
||||||
|
|
||||||
|
## [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
|
## [5.3.5] - 17.01.2025
|
||||||
|
|
||||||
## Fixed
|
## Fixed
|
||||||
|
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);
|
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 =>
|
modelBuilder.Entity("NadekoBot.Services.GreetSettings", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
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");
|
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 =>
|
modelBuilder.Entity("NadekoBot.Services.GreetSettings", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
|
@@ -13,6 +13,7 @@ using System.Globalization;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using NadekoBot.Modules.Gambling.Rps;
|
using NadekoBot.Modules.Gambling.Rps;
|
||||||
using NadekoBot.Common.TypeReaders;
|
using NadekoBot.Common.TypeReaders;
|
||||||
|
using NadekoBot.Modules.Games;
|
||||||
using NadekoBot.Modules.Patronage;
|
using NadekoBot.Modules.Patronage;
|
||||||
using SixLabors.Fonts;
|
using SixLabors.Fonts;
|
||||||
using SixLabors.Fonts.Unicode;
|
using SixLabors.Fonts.Unicode;
|
||||||
@@ -40,6 +41,7 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||||||
private readonly IPatronageService _ps;
|
private readonly IPatronageService _ps;
|
||||||
private readonly RakebackService _rb;
|
private readonly RakebackService _rb;
|
||||||
private readonly IBotCache _cache;
|
private readonly IBotCache _cache;
|
||||||
|
private readonly CaptchaService _captchaService;
|
||||||
|
|
||||||
public Gambling(
|
public Gambling(
|
||||||
IGamblingService gs,
|
IGamblingService gs,
|
||||||
@@ -54,7 +56,8 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||||||
IPatronageService patronage,
|
IPatronageService patronage,
|
||||||
GamblingTxTracker gamblingTxTracker,
|
GamblingTxTracker gamblingTxTracker,
|
||||||
RakebackService rb,
|
RakebackService rb,
|
||||||
IBotCache cache)
|
IBotCache cache,
|
||||||
|
CaptchaService captchaService)
|
||||||
: base(configService)
|
: base(configService)
|
||||||
{
|
{
|
||||||
_gs = gs;
|
_gs = gs;
|
||||||
@@ -66,6 +69,7 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||||||
_gamblingTxTracker = gamblingTxTracker;
|
_gamblingTxTracker = gamblingTxTracker;
|
||||||
_rb = rb;
|
_rb = rb;
|
||||||
_cache = cache;
|
_cache = cache;
|
||||||
|
_captchaService = captchaService;
|
||||||
_ps = patronage;
|
_ps = patronage;
|
||||||
_rng = new NadekoRandom();
|
_rng = new NadekoRandom();
|
||||||
|
|
||||||
@@ -154,49 +158,42 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||||||
}
|
}
|
||||||
else if (Config.Timely.ProtType == TimelyProt.Captcha)
|
else if (Config.Timely.ProtType == TimelyProt.Captcha)
|
||||||
{
|
{
|
||||||
var password = await GetUserTimelyPassword(ctx.User.Id);
|
var password = await _captchaService.GetUserCaptcha(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;
|
|
||||||
}
|
|
||||||
|
|
||||||
await ClearUserTimelyPassword(ctx.User.Id);
|
if (password is not null)
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
{
|
||||||
_ = 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();
|
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)
|
private Image<Rgba32> GetPasswordImage(string password)
|
||||||
{
|
{
|
||||||
var img = new Image<Rgba32>(50, 24);
|
var img = new Image<Rgba32>(50, 24);
|
||||||
|
@@ -1,4 +1,6 @@
|
|||||||
using SixLabors.Fonts;
|
using NadekoBot.Db.Models;
|
||||||
|
using NadekoBot.Modules.Patronage;
|
||||||
|
using SixLabors.Fonts;
|
||||||
using SixLabors.Fonts.Unicode;
|
using SixLabors.Fonts.Unicode;
|
||||||
using SixLabors.ImageSharp;
|
using SixLabors.ImageSharp;
|
||||||
using SixLabors.ImageSharp.Drawing.Processing;
|
using SixLabors.ImageSharp.Drawing.Processing;
|
||||||
@@ -9,7 +11,7 @@ using Color = SixLabors.ImageSharp.Color;
|
|||||||
|
|
||||||
namespace NadekoBot.Modules.Games;
|
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();
|
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));
|
var num = _rng.Next((int)Math.Pow(31, 2), (int)Math.Pow(32, 3));
|
||||||
return new kwum(num).ToString();
|
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;
|
using Format = Discord.Format;
|
||||||
|
|
||||||
namespace NadekoBot.Modules.Games;
|
namespace NadekoBot.Modules.Games;
|
||||||
@@ -9,8 +10,10 @@ public partial class Games
|
|||||||
FishService fs,
|
FishService fs,
|
||||||
FishConfigService fcs,
|
FishConfigService fcs,
|
||||||
IBotCache cache,
|
IBotCache cache,
|
||||||
CaptchaService service) : NadekoModule
|
CaptchaService captchaService) : NadekoModule
|
||||||
{
|
{
|
||||||
|
private static readonly NadekoRandom _rng = new();
|
||||||
|
|
||||||
private TypedKey<bool> FishingWhitelistKey(ulong userId)
|
private TypedKey<bool> FishingWhitelistKey(ulong userId)
|
||||||
=> new($"fishingwhitelist:{userId}");
|
=> new($"fishingwhitelist:{userId}");
|
||||||
|
|
||||||
@@ -20,29 +23,39 @@ public partial class Games
|
|||||||
var cRes = await cache.GetAsync(FishingWhitelistKey(ctx.User.Id));
|
var cRes = await cache.GetAsync(FishingWhitelistKey(ctx.User.Id));
|
||||||
if (cRes.TryPickT1(out _, out _))
|
if (cRes.TryPickT1(out _, out _))
|
||||||
{
|
{
|
||||||
var password = await GetUserCaptcha(ctx.User.Id);
|
var password = await captchaService.GetUserCaptcha(ctx.User.Id);
|
||||||
var img = service.GetPasswordImage(password);
|
if (password is not null)
|
||||||
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);
|
var img = captchaService.GetPasswordImage(password);
|
||||||
if (userInput?.ToLowerInvariant() != password?.ToLowerInvariant())
|
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
|
// whitelist the user for 30 minutes
|
||||||
await cache.AddAsync(FishingWhitelistKey(ctx.User.Id), true, TimeSpan.FromMinutes(30));
|
await cache.AddAsync(FishingWhitelistKey(ctx.User.Id), true, TimeSpan.FromMinutes(30));
|
||||||
// reset the password
|
// reset the password
|
||||||
await ClearUserCaptcha(ctx.User.Id);
|
await captchaService.ClearUserCaptcha(ctx.User.Id);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_ = captcha.DeleteAsync();
|
_ = captcha.DeleteAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,12 +89,18 @@ public partial class Games
|
|||||||
return;
|
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()
|
await Response()
|
||||||
.Embed(CreateEmbed()
|
.Embed(CreateEmbed()
|
||||||
.WithOkColor()
|
.WithOkColor()
|
||||||
.WithAuthor(ctx.User)
|
.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.fish_quality), GetStarText(res.Stars, res.Fish.Stars), true)
|
||||||
.AddField(GetText(strs.desc), res.Fish.Fluff, true)
|
.AddField(GetText(strs.desc), res.Fish.Fluff, true)
|
||||||
.WithThumbnailUrl(res.Fish.Image))
|
.WithThumbnailUrl(res.Fish.Image))
|
||||||
@@ -117,8 +136,8 @@ public partial class Games
|
|||||||
|
|
||||||
var fishes = await fs.GetAllFish();
|
var fishes = await fs.GetAllFish();
|
||||||
|
|
||||||
Log.Information(fishes.Count.ToString());
|
|
||||||
var catches = await fs.GetUserCatches(ctx.User.Id);
|
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);
|
var catchDict = catches.ToDictionary(x => x.FishId, x => x);
|
||||||
|
|
||||||
@@ -130,7 +149,10 @@ public partial class Games
|
|||||||
.Page((fs, i) =>
|
.Page((fs, i) =>
|
||||||
{
|
{
|
||||||
var eb = CreateEmbed()
|
var eb = CreateEmbed()
|
||||||
.WithOkColor();
|
.WithDescription($"🧠 **Skill:** {skill} / {maxSkill}")
|
||||||
|
.WithAuthor(ctx.User)
|
||||||
|
.WithTitle(GetText(strs.fish_list_title))
|
||||||
|
.WithOkColor();
|
||||||
|
|
||||||
foreach (var f in fs)
|
foreach (var f in fs)
|
||||||
{
|
{
|
||||||
@@ -224,48 +246,9 @@ public partial class Games
|
|||||||
|
|
||||||
return sb.ToString();
|
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
|
public enum FishingSpot
|
||||||
{
|
{
|
||||||
Ocean,
|
Ocean,
|
||||||
|
@@ -4,6 +4,9 @@ public sealed class FishResult
|
|||||||
{
|
{
|
||||||
public required FishData Fish { get; init; }
|
public required FishData Fish { get; init; }
|
||||||
public int Stars { 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;
|
public readonly record struct AlreadyFishing;
|
||||||
|
|
||||||
|
@@ -1,11 +1,16 @@
|
|||||||
using LinqToDB;
|
using LinqToDB;
|
||||||
using LinqToDB.EntityFrameworkCore;
|
using LinqToDB.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
namespace NadekoBot.Modules.Games;
|
namespace NadekoBot.Modules.Games;
|
||||||
|
|
||||||
public sealed class FishService(FishConfigService fcs, IBotCache cache, DbService db) : INService
|
public sealed class FishService(FishConfigService fcs, IBotCache cache, DbService db) : INService
|
||||||
{
|
{
|
||||||
|
public const double MAX_SKILL = 100;
|
||||||
|
|
||||||
private Random _rng = new Random();
|
private Random _rng = new Random();
|
||||||
|
|
||||||
private static TypedKey<bool> FishingKey(ulong userId)
|
private static TypedKey<bool> FishingKey(ulong userId)
|
||||||
@@ -28,20 +33,104 @@ public sealed class FishService(FishConfigService fcs, IBotCache cache, DbServic
|
|||||||
var conf = fcs.Data;
|
var conf = fcs.Data;
|
||||||
await Task.Delay(TimeSpan.FromSeconds(duration));
|
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
|
// 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;
|
var typeRoll = _rng.NextDouble() * totalChance;
|
||||||
|
|
||||||
if (typeRoll < conf.Chance.Nothing)
|
if (typeRoll < nothingChance)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var items = typeRoll < conf.Chance.Nothing + conf.Chance.Fish
|
var items = typeRoll < nothingChance + fishChance
|
||||||
? conf.Fish
|
? conf.Fish
|
||||||
: conf.Trash;
|
: 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)
|
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)
|
public FishingSpot GetSpot(ulong channelId)
|
||||||
{
|
{
|
||||||
var cid = (channelId >> 22 >> 8) % 10;
|
var cid = (channelId >> 22 >> 29) % 10;
|
||||||
|
|
||||||
return cid switch
|
return cid switch
|
||||||
{
|
{
|
||||||
@@ -159,7 +248,6 @@ public sealed class FishService(FishConfigService fcs, IBotCache cache, DbServic
|
|||||||
return FishingTime.Day;
|
return FishingTime.Day;
|
||||||
|
|
||||||
return FishingTime.Dusk;
|
return FishingTime.Dusk;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private const int WEATHER_PERIODS_PER_DAY = 12;
|
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()
|
public async Task<List<FishData>> GetAllFish()
|
||||||
{
|
{
|
||||||
await Task.Yield();
|
await Task.Yield();
|
||||||
|
|
||||||
var conf = fcs.Data;
|
var conf = fcs.Data;
|
||||||
return conf.Fish.Concat(conf.Trash).ToList();
|
return conf.Fish.Concat(conf.Trash).ToList();
|
||||||
}
|
}
|
||||||
@@ -312,4 +400,22 @@ public sealed class FishService(FishConfigService fcs, IBotCache cache, DbServic
|
|||||||
|
|
||||||
return catches;
|
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>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>true</ImplicitUsings>
|
<ImplicitUsings>true</ImplicitUsings>
|
||||||
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||||
<Version>5.3.5</Version>
|
<Version>5.3.6</Version>
|
||||||
|
|
||||||
<!-- Output/build -->
|
<!-- Output/build -->
|
||||||
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>
|
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>
|
||||||
|
@@ -1169,5 +1169,7 @@
|
|||||||
"fish_weather_duration": "Each weather period lasts for {0} hours.",
|
"fish_weather_duration": "Each weather period lasts for {0} hours.",
|
||||||
"fish_weather_current": "Current",
|
"fish_weather_current": "Current",
|
||||||
"fish_weather_forecast": "Forecast",
|
"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