mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-10 09:18:27 -04:00
Compare commits
1 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
e40c9335c1 |
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
|
||||
|
||||
## [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
|
||||
|
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")
|
||||
|
@@ -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);
|
||||
|
@@ -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.5</Version>
|
||||
<Version>5.3.6</Version>
|
||||
|
||||
<!-- Output/build -->
|
||||
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>
|
||||
|
@@ -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