Added .fish command

This commit is contained in:
Toastie 2025-01-14 19:03:59 +13:00
parent c574956d94
commit b71e8969a0
Signed by: toastie_t0ast
GPG key ID: 0861BE54AD481DC7
23 changed files with 15047 additions and 6612 deletions

View file

@ -0,0 +1,88 @@
// using System;
// using System.Collections.Generic;
// using System.Diagnostics;
// using System.IO;
// using System.Linq;
// using Ellie.Common;
// using EllieBot.Modules.Games;
// using NUnit.Framework;
//
// namespace EllieBot.Tests;
//
// public class FishTests
// {
// [Test]
// public void TestWeather()
// {
// var fs = new FishService(null, null);
//
// var rng = new Random();
//
// // output = @"ro+dD:bN0uVqV3ZOAv6r""EFeA'A]u]uSyz2Qd'r#0Vf:5zOX\VgSsF8LgRCL/uOW";
// while (true)
// {
// var output = "";
// for (var i = 0; i < 64; i++)
// {
// var c = (char)rng.Next(33, 123);
// output += c;
// }
//
// output = "";
// var weathers = new List<FishingWeather>();
// for (var i = 0; i < 1_000_000; i++)
// {
// var w = fs.GetWeather(DateTime.UtcNow.AddHours(6 * i), output);
// weathers.Add(w);
// }
//
// var vals = weathers.GroupBy(x => x)
// .ToDictionary(x => x.Key, x => x.Count());
//
// var str = weathers.Select(x => (int)x).Join("");
// var maxLength = MaxLength(str);
//
// if (maxLength < 12)
// {
// foreach (var v in vals)
// {
// Console.WriteLine($"{v.Key}: {v.Value}");
// }
//
// Console.WriteLine(output);
// Console.WriteLine(maxLength);
//
// File.WriteAllText("data.txt", weathers.Select(x => (int)x).Join(""));
//
// break;
// }
// }
// }
//
// // string with same characters
// static int MaxLength(String s)
// {
// int ans = 1, temp = 1;
//
// // Traverse the string
// for (int i = 1; i < s.Length; i++)
// {
// // If character is same as
// // previous increment temp value
// if (s[i] == s[i - 1])
// {
// ++temp;
// }
// else
// {
// ans = Math.Max(ans, temp);
// temp = 1;
// }
// }
//
// ans = Math.Max(ans, temp);
//
// // Return the required answer
// return ans;
// }
// }

View file

@ -1,4 +1,5 @@
using Ellie.Common;
using System;
using Ellie.Common;
using NUnit.Framework;
namespace EllieBot.Tests
@ -120,5 +121,12 @@ namespace EllieBot.Tests
num = new kwum(int.MaxValue);
Assert.AreEqual("3zzzzzz", num.ToString());
}
[Test]
public void TestPower()
{
var num = new kwum((int)Math.Pow(32, 2));
Assert.AreEqual("322", num.ToString());
}
}
}

View file

@ -74,6 +74,9 @@ public abstract class EllieContext : DbContext
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// load all entities from current assembly
modelBuilder.ApplyConfigurationsFromAssembly(typeof(EllieContext).Assembly);
#region Notify
modelBuilder.Entity<Notify>(e =>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,39 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace EllieBot.Migrations.PostgreSql
{
/// <inheritdoc />
public partial class fishes : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "fishcatch",
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),
fishid = table.Column<int>(type: "integer", nullable: false),
count = table.Column<int>(type: "integer", nullable: false),
maxstars = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_fishcatch", x => x.id);
table.UniqueConstraint("ak_fishcatch_userid_fishid", x => new { x.userid, x.fishid });
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "fishcatch");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,38 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace EllieBot.Migrations
{
/// <inheritdoc />
public partial class fishes : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "FishCatch",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
UserId = table.Column<ulong>(type: "INTEGER", nullable: false),
FishId = table.Column<int>(type: "INTEGER", nullable: false),
Count = table.Column<int>(type: "INTEGER", nullable: false),
MaxStars = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_FishCatch", x => x.Id);
table.UniqueConstraint("AK_FishCatch_UserId_FishId", x => new { x.UserId, x.FishId });
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "FishCatch");
}
}
}

View file

@ -0,0 +1,55 @@
using SixLabors.Fonts;
using SixLabors.Fonts.Unicode;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using Color = SixLabors.ImageSharp.Color;
namespace EllieBot.Modules.Games;
public sealed class CaptchaService(FontProvider fonts) : IEService
{
private readonly EllieRandom _rng = new();
public Image<Rgba32> GetPasswordImage(string password)
{
var img = new Image<Rgba32>(50, 24);
var font = fonts.NotoSans.CreateFont(22);
var outlinePen = new SolidPen(Color.Black, 0.5f);
var strikeoutRun = new RichTextRun
{
Start = 0,
End = password.GetGraphemeCount(),
Font = font,
StrikeoutPen = new SolidPen(Color.White, 4),
TextDecorations = TextDecorations.Strikeout
};
// draw password on the image
img.Mutate(x =>
{
DrawTextExtensions.DrawText(x,
new RichTextOptions(font)
{
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
FallbackFontFamilies = fonts.FallBackFonts,
Origin = new(25, 12),
TextRuns = [strikeoutRun]
},
password,
Brushes.Solid(Color.White),
outlinePen);
});
return img;
}
public string GeneratePassword()
{
var num = _rng.Next((int)Math.Pow(31, 2), (int)Math.Pow(32, 3));
return new kwum(num).ToString();
}
}

View file

@ -0,0 +1,27 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using System.ComponentModel.DataAnnotations;
namespace EllieBot.Modules.Games;
public sealed class FishCatch
{
[Key]
public int Id { get; set; }
public ulong UserId { get; set; }
public int FishId { get; set; }
public int Count { get; set; }
public int MaxStars { get; set; }
}
public sealed class FishCatchConfiguration : IEntityTypeConfiguration<FishCatch>
{
public void Configure(EntityTypeBuilder<FishCatch> builder)
{
builder.HasAlternateKey(x => new
{
x.UserId,
x.FishId
});
}
}

View file

@ -0,0 +1,8 @@
namespace EllieBot.Modules.Games;
public sealed class FishChance
{
public int Fish { get; set; } = 75;
public int Trash { get; set; } = 20;
public int Nothing { get; set; } = 0;
}

View file

@ -0,0 +1,292 @@
using System.Text;
using Format = Discord.Format;
namespace EllieBot.Modules.Games;
public partial class Games
{
public class FishCommands(
FishService fs,
FishConfigService fcs,
IBotCache cache,
CaptchaService service) : EllieModule
{
private TypedKey<bool> FishingWhitelistKey(ulong userId)
=> new($"fishingwhitelist:{userId}");
[Cmd]
public async Task Fish()
{
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 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();
}
}
var fishResult = await fs.FishAsync(ctx.User.Id, ctx.Channel.Id);
if (fishResult.TryPickT1(out _, out var fishTask))
{
return;
}
var currentWeather = fs.GetCurrentWeather();
var currentTod = fs.GetTime();
var spot = fs.GetSpot(ctx.Channel.Id);
var msg = await Response()
.Embed(CreateEmbed()
.WithPendingColor()
.WithAuthor(ctx.User)
.WithDescription(GetText(strs.fish_waiting))
.AddField(GetText(strs.fish_spot), GetSpotEmoji(spot) + " " + spot.ToString(), true)
.AddField(GetText(strs.fish_weather),
GetWeatherEmoji(currentWeather) + " " + currentWeather,
true)
.AddField(GetText(strs.fish_tod), GetTodEmoji(currentTod) + " " + currentTod, true))
.SendAsync();
var res = await fishTask;
if (res is null)
{
await Response().Error(strs.fish_nothing).SendAsync();
return;
}
await Response()
.Embed(CreateEmbed()
.WithOkColor()
.WithAuthor(ctx.User)
.WithDescription(GetText(strs.fish_caught(Format.Bold(res.Fish.Name))))
.AddField(GetText(strs.fish_quality), GetStarText(res.Stars, res.Fish.Stars), true)
.AddField(GetText(strs.desc), res.Fish.Fluff, true)
.WithThumbnailUrl(res.Fish.Image))
.SendAsync();
await msg.DeleteAsync();
}
[Cmd]
public async Task FishSpot()
{
var ws = fs.GetWeatherForPeriods(7);
var spot = fs.GetSpot(ctx.Channel.Id);
var time = fs.GetTime();
await Response()
.Embed(CreateEmbed()
.WithOkColor()
.WithDescription(GetText(strs.fish_weather_duration(fs.GetWeatherPeriodDuration())))
.AddField(GetText(strs.fish_spot), GetSpotEmoji(spot) + " " + spot, true)
.AddField(GetText(strs.fish_tod), GetTodEmoji(time) + " " + time, true)
.AddField(GetText(strs.fish_weather_forecast),
ws.Select(x => GetWeatherEmoji(x)).Join(""),
true))
.SendAsync();
}
[Cmd]
public async Task Fishlist(int page = 1)
{
if (--page < 0)
return;
var fishes = await fs.GetAllFish();
Log.Information(fishes.Count.ToString());
var catches = await fs.GetUserCatches(ctx.User.Id);
var catchDict = catches.ToDictionary(x => x.FishId, x => x);
await Response()
.Paginated()
.Items(fishes)
.PageSize(9)
.CurrentPage(page)
.Page((fs, i) =>
{
var eb = CreateEmbed()
.WithOkColor();
foreach (var f in fs)
{
if (catchDict.TryGetValue(f.Id, out var c))
{
eb.AddField(f.Name,
GetFishEmoji(f, c.Count)
+ " "
+ GetSpotEmoji(f.Spot)
+ GetTodEmoji(f.Time)
+ GetWeatherEmoji(f.Weather)
+ "\n"
+ GetStarText(c.MaxStars, f.Stars)
+ "\n"
+ Format.Italics(f.Fluff),
true);
}
else
{
eb.AddField("?", GetFishEmoji(null, 0) + "\n" + GetStarText(0, f.Stars), true);
}
}
return eb;
})
.SendAsync();
}
private string GetFishEmoji(FishData? fish, int count)
{
if (fish is null)
return "";
return fish.Emoji + " x" + count;
}
private string GetSpotEmoji(FishingSpot? spot)
{
if (spot is not FishingSpot fs)
return string.Empty;
var conf = fcs.Data;
return conf.SpotEmojis[(int)fs];
}
private string GetTodEmoji(FishingTime? fishTod)
{
return fishTod switch
{
FishingTime.Night => "🌑",
FishingTime.Dawn => "🌅",
FishingTime.Dusk => "🌆",
FishingTime.Day => "☀️",
_ => ""
};
}
private string GetWeatherEmoji(FishingWeather? w)
=> w switch
{
FishingWeather.Rain => "🌧️",
FishingWeather.Snow => "❄️",
FishingWeather.Storm => "⛈️",
FishingWeather.Clear => "☀️",
_ => ""
};
private string GetStarText(int resStars, int fishStars)
{
if (resStars == fishStars)
{
return MultiplyStars(fcs.Data.StarEmojis[^1], fishStars);
}
var c = fcs.Data;
var starsp1 = MultiplyStars(c.StarEmojis[resStars], resStars);
var starsp2 = MultiplyStars(c.StarEmojis[0], fishStars - resStars);
return starsp1 + starsp2;
}
private string MultiplyStars(string starEmoji, int count)
{
var sb = new StringBuilder();
for (var i = 0; i < count; i++)
{
sb.Append(starEmoji);
}
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,
River,
Lake,
Swamp,
Reef
}
public enum FishingTime
{
Night,
Dawn,
Day,
Dusk
}
public enum FishingWeather
{
Clear,
Rain,
Storm,
Snow
}

View file

@ -0,0 +1,19 @@
using Cloneable;
using EllieBot.Common.Yml;
namespace EllieBot.Modules.Games;
[Cloneable]
public sealed partial class FishConfig : ICloneable<FishConfig>
{
[Comment("DO NOT CHANGE")]
public int Version { get; set; } = 1;
public string WeatherSeed { get; set; } = string.Empty;
public List<string> StarEmojis { get; set; } = new();
public List<string> SpotEmojis { get; set; } = new();
public FishChance Chance { get; set; } = new FishChance();
public List<FishData> Fish { get; set; } = new();
public List<FishData> Trash { get; set; } = new();
}

View file

@ -0,0 +1,19 @@
using EllieBot.Common.Configs;
namespace EllieBot.Modules.Games;
public sealed class FishConfigService : ConfigServiceBase<FishConfig>
{
private static string FILE_PATH = "data/fish.yml";
private static readonly TypedKey<FishConfig> _changeKey = new("config.fish.updated");
public override string Name
=> "fishing";
public FishConfigService(
IConfigSeria serializer,
IPubSub pubSub)
: base(FILE_PATH, serializer, pubSub, _changeKey)
{
}
}

View file

@ -0,0 +1,16 @@
namespace EllieBot.Modules.Games;
public class FishData
{
public required int Id { get; set; }
public required string Name { get; set; }
public FishingWeather? Weather { get; set; }
public FishingSpot? Spot { get; set; }
public FishingTime? Time { get; set; }
public required double Chance { get; set; }
public required int Stars { get; set; }
public required string Fluff { get; set; }
public List<string>? Condition { get; set; }
public string? Image { get; init; }
public string? Emoji { get; set; }
}

View file

@ -0,0 +1,9 @@
namespace EllieBot.Modules.Games;
public sealed class FishResult
{
public required FishData Fish { get; init; }
public int Stars { get; init; }
}
public readonly record struct AlreadyFishing;

View file

@ -0,0 +1,318 @@
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using System.Security.Cryptography;
namespace EllieBot.Modules.Games;
public sealed class FishService(FishConfigService fcs, IBotCache cache, DbService db) : IEService
{
private Random _rng = new Random();
private static TypedKey<bool> FishingKey(ulong userId)
=> new($"fishing:{userId}");
public async Task<OneOf.OneOf<Task<FishResult?>, AlreadyFishing>> FishAsync(ulong userId, ulong channelId)
{
var duration = _rng.Next(1, 9);
if (!await cache.AddAsync(FishingKey(userId), true, TimeSpan.FromSeconds(duration), overwrite: false))
{
return new AlreadyFishing();
}
return TryFishAsync(userId, channelId, duration);
}
private async Task<FishResult?> TryFishAsync(ulong userId, ulong channelId, int duration)
{
var conf = fcs.Data;
await Task.Delay(TimeSpan.FromSeconds(duration));
// first roll whether it's fish, trash or nothing
var totalChance = conf.Chance.Fish + conf.Chance.Trash + conf.Chance.Nothing;
var typeRoll = _rng.NextDouble() * totalChance;
if (typeRoll < conf.Chance.Nothing)
{
return null;
}
var items = typeRoll < conf.Chance.Nothing + conf.Chance.Fish
? conf.Fish
: conf.Trash;
return await FishAsyncInternal(userId, channelId, items);
}
private async Task<FishResult?> FishAsyncInternal(ulong userId, ulong channelId, List<FishData> items)
{
var filteredItems = new List<FishData>();
var loc = GetSpot(channelId);
var time = GetTime();
var w = GetWeather(DateTime.UtcNow);
foreach (var item in items)
{
if (item.Condition is { Count: > 0 })
{
if (!item.Condition.Any(x => channelId.ToString().EndsWith(x)))
{
continue;
}
}
if (item.Spot is not null && item.Spot != loc)
continue;
if (item.Time is not null && item.Time != time)
continue;
if (item.Weather is not null && item.Weather != w)
continue;
filteredItems.Add(item);
Log.Information("Added {FishName} to filtered items", item.Name);
}
var maxSum = filteredItems.Sum(x => x.Chance * 100);
var roll = _rng.NextDouble() * maxSum;
Log.Information("Roll: {Roll}, MaxSum: {MaxSum}", roll, maxSum);
FishResult? caught = null;
var curSum = 0d;
foreach (var i in filteredItems)
{
curSum += i.Chance * 100;
if (roll < curSum)
{
caught = new FishResult()
{
Fish = i,
Stars = GetRandomStars(i.Stars),
};
break;
}
}
if (caught is not null)
{
await using var uow = db.GetDbContext();
await uow.GetTable<FishCatch>()
.InsertOrUpdateAsync(() => new FishCatch()
{
UserId = userId,
FishId = caught.Fish.Id,
MaxStars = caught.Stars,
Count = 1
},
(old) => new()
{
Count = old.Count + 1,
MaxStars = Math.Max((int)old.MaxStars, caught.Stars),
},
() => new()
{
FishId = caught.Fish.Id,
UserId = userId
});
return caught;
}
Log.Error(
"Something went wrong in the fish command, no fish with sufficient chance was found, Roll: {Roll}, MaxSum: {MaxSum}",
roll,
maxSum);
return null;
}
public FishingSpot GetSpot(ulong channelId)
{
var cid = (channelId >> 22 >> 8) & 1;
return cid switch
{
< 1 => FishingSpot.Reef,
< 3 => FishingSpot.River,
< 5 => FishingSpot.Lake,
< 7 => FishingSpot.Swamp,
_ => FishingSpot.Ocean,
};
}
public FishingTime GetTime()
{
var hour = DateTime.UtcNow.Hour % 12;
if (hour < 3)
return FishingTime.Night;
if (hour < 4)
return FishingTime.Dawn;
if (hour < 11)
return FishingTime.Day;
return FishingTime.Dusk;
}
private const int WEATHER_PERIODS_PER_DAY = 12;
public IReadOnlyList<FishingWeather> GetWeatherForPeriods(int periods)
{
var now = DateTime.UtcNow;
var result = new FishingWeather[periods];
for (var i = 0; i < periods; i++)
{
result[i] = GetWeather(now.AddHours(i * GetWeatherPeriodDuration()));
}
return result;
}
public FishingWeather GetCurrentWeather()
=> GetWeather(DateTime.UtcNow);
public FishingWeather GetWeather(DateTime time)
=> GetWeather(time, fcs.Data.WeatherSeed);
private FishingWeather GetWeather(DateTime time, string seed)
{
var year = time.Year;
var dayOfYear = time.DayOfYear;
var hour = time.Hour;
var num = (year * 100_000) + (dayOfYear * 100) + (hour / GetWeatherPeriodDuration());
Span<byte> dataArray = stackalloc byte[4];
BitConverter.TryWriteBytes(dataArray, num);
Span<byte> seedArray = stackalloc byte[seed.Length];
for (var index = 0; index < seed.Length; index++)
{
var c = seed[index];
seedArray[index] = (byte)c;
}
Span<byte> arr = stackalloc byte[dataArray.Length + seedArray.Length];
dataArray.CopyTo(arr);
seedArray.CopyTo(arr[dataArray.Length..]);
using var algo = SHA512.Create();
Span<byte> hash = stackalloc byte[64];
algo.TryComputeHash(arr, hash, out _);
byte reduced = 0;
foreach (var u in hash)
reduced ^= u;
var r = reduced % 16;
// return (FishingWeather)r;
return r switch
{
< 5 => FishingWeather.Clear,
< 9 => FishingWeather.Rain,
< 13 => FishingWeather.Storm,
_ => FishingWeather.Snow
};
}
/// <summary>
/// Returns a random number of stars between 1 and maxStars
/// if maxStars == 1, returns 1
/// if maxStars == 2, returns 1 (66%) or 2 (33%)
/// if maxStars == 3, returns 1 (65%) or 2 (25%) or 3 (10%)
/// if maxStars == 5, returns 1 (40%) or 2 (30%) or 3 (15%) or 4 (10%) or 5 (5%)
/// </summary>
/// <param name="maxStars">Max Number of stars to generate</param>
/// <returns>Random number of stars</returns>
private int GetRandomStars(int maxStars)
{
if (maxStars == 1)
return 1;
if (maxStars == 2)
{
// 66% chance of 1 star, 33% chance of 2 stars
return _rng.NextDouble() < 0.66 ? 1 : 2;
}
if (maxStars == 3)
{
// 65% chance of 1 star, 25% chance of 2 stars, 10% chance of 3 stars
var r = _rng.NextDouble();
if (r < 0.65)
return 1;
if (r < 0.9)
return 2;
return 3;
}
if (maxStars == 4)
{
// this should never happen
// 50% chance of 1 star, 25% chance of 2 stars, 18% chance of 3 stars, 7% chance of 4 stars
var r = _rng.NextDouble();
if (r < 0.5)
return 1;
if (r < 0.7)
return 2;
if (r < 0.85)
return 3;
return 4;
}
if (maxStars == 5)
{
// 40% chance of 1 star, 30% chance of 2 stars, 15% chance of 3 stars, 10% chance of 4 stars, 5% chance of 5 stars
var r = _rng.NextDouble();
if (r < 0.4)
return 1;
if (r < 0.7)
return 2;
if (r < 0.9)
return 3;
if (r < 0.95)
return 4;
return 5;
}
return 1;
}
public int GetWeatherPeriodDuration()
=> 24 / WEATHER_PERIODS_PER_DAY;
public async Task<List<FishData>> GetAllFish()
{
await Task.Yield();
var conf = fcs.Data;
return conf.Fish.Concat(conf.Trash).ToList();
}
public async Task<List<FishCatch>> GetUserCatches(ulong userId)
{
await using var ctx = db.GetDbContext();
var catches = await ctx.GetTable<FishCatch>()
.Where(x => x.UserId == userId)
.ToListAsyncLinqToDB();
return catches;
}
}

View file

@ -54,13 +54,9 @@ public sealed class EllieRandom : Random
{
var bytes = new byte[sizeof(double)];
_rng.GetBytes(bytes);
return Math.Abs((BitConverter.ToDouble(bytes, 0) / double.MaxValue) + 1);
return Math.Abs((BitConverter.ToDouble(bytes, 0) / (double.MaxValue + 1)));
}
public override double NextDouble()
{
var bytes = new byte[sizeof(double)];
_rng.GetBytes(bytes);
return BitConverter.ToDouble(bytes, 0);
}
=> Sample();
}

View file

@ -1559,4 +1559,17 @@ notifyclear:
- notifclr
winlb:
- winlb
- wins
- wins
fish:
- fish
- fi
fishlist:
- fishlist
- fili
- fishes
- fil
- fishlist
fishspot:
- fishspot
- fisp
- fish?

View file

@ -0,0 +1,43 @@
# DO NOT CHANGE
version: 1
weatherSeed: "w%29';^eGE)9oWHM(aI9I;%1[.r^z2ZS7ShV,l')o(e%#\"hVzb>oxQq^`.&/7srh"
chance:
fish: 80
trash: 15
nothing: 5
starEmojis:
- <:emptystar:1326838565786877962>
- <:onestar:1326838456739168361>
- <:twostar:1326838508198957107>
- <:threestar:1326838525601251429>
- <:fourstar:1326838552520294462>
spotEmojis:
- <:ocean:1328519734953771120>
- <:river:1328519754620862504>
- <:lake:1328315260561788989>
- <:swamp:1328519766083633224>
- <:reef:1328519744646545421>
fish:
- name: Bass
id: 0
weather:
spot:
time:
chance: 100
stars: 4
fluff: Very common.
condition:
image: https://cdn.nadeko.bot/fish/bass.png
emoji: "<:bass:1328520376892002386>"
trash:
- name: Plastic Bag
id: 1002
weather:
spot:
time:
chance: 50
stars: 4
fluff: "Trophy of your contribution to the environment."
condition:
image: https://cdn.nadeko.bot/fish/plasticbag.png
emoji: "<:plasticbag:1328520895454515211>"

View file

@ -4904,4 +4904,30 @@ winlb:
- '5'
params:
- page:
desc: "The optional page to display."
desc: "The optional page to display."
fish:
desc: |-
Attempt to catch a fish.
Different fish live in different places, at different times and during different times of the day.
ex:
- ''
params:
- { }
fishlist:
desc: |-
Look at your fish catalogue.
Shows how many of each fish you caught and what was the highest quality.
For each caught fish, it also shows its required spot, time of day and weather.
ex:
- ''
params:
- { }
- page:
desc: "The optional page to display."
fishspot:
desc: |-
Shows information about the current fish spot, weather and time.
ex:
- ''
params:
- { }

View file

@ -1159,5 +1159,15 @@
"notify_desc_removerolerew": "Triggers when a user loses a role as a reward for reaching a level (xprew).",
"notify_desc_not_found": "No description found for this notify event. Please report this.",
"winlb": "Biggest Wins Leaderboard",
"no_banner": "No banner set."
"no_banner": "No banner set.",
"fish_nothing": "You caught nothing, try again.",
"fish_caught": "You caught a {0}!",
"fish_quality": "Quality",
"fish_spot": "Spot",
"fish_waiting": "Fishing...",
"fish_weather": "Weather",
"fish_weather_duration": "Each weather period lasts for {0} hours.",
"fish_weather_current": "Current",
"fish_weather_forecast": "Forecast",
"fish_tod": "Time of Day"
}