mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-11 17:58:26 -04:00
add: Added .fish command
This commit is contained in:
56
src/NadekoBot/Modules/Games/Fish/CaptchaService.cs
Normal file
56
src/NadekoBot/Modules/Games/Fish/CaptchaService.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
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 NadekoBot.Modules.Games;
|
||||
|
||||
public sealed class CaptchaService(FontProvider fonts) : INService
|
||||
{
|
||||
private readonly NadekoRandom _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();
|
||||
}
|
||||
}
|
27
src/NadekoBot/Modules/Games/Fish/FishCatch.cs
Normal file
27
src/NadekoBot/Modules/Games/Fish/FishCatch.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace NadekoBot.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
|
||||
});
|
||||
}
|
||||
}
|
8
src/NadekoBot/Modules/Games/Fish/FishChance.cs
Normal file
8
src/NadekoBot/Modules/Games/Fish/FishChance.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace NadekoBot.Modules.Games;
|
||||
|
||||
public sealed class FishChance
|
||||
{
|
||||
public int Fish { get; set; } = 75;
|
||||
public int Trash { get; set; } = 20;
|
||||
public int Nothing { get; set; } = 0;
|
||||
}
|
292
src/NadekoBot/Modules/Games/Fish/FishCommands.cs
Normal file
292
src/NadekoBot/Modules/Games/Fish/FishCommands.cs
Normal file
@@ -0,0 +1,292 @@
|
||||
using System.Text;
|
||||
using Format = Discord.Format;
|
||||
|
||||
namespace NadekoBot.Modules.Games;
|
||||
|
||||
public partial class Games
|
||||
{
|
||||
public class FishCommands(
|
||||
FishService fs,
|
||||
FishConfigService fcs,
|
||||
IBotCache cache,
|
||||
CaptchaService service) : NadekoModule
|
||||
{
|
||||
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
|
||||
}
|
19
src/NadekoBot/Modules/Games/Fish/FishConfig.cs
Normal file
19
src/NadekoBot/Modules/Games/Fish/FishConfig.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using Cloneable;
|
||||
using NadekoBot.Common.Yml;
|
||||
|
||||
namespace NadekoBot.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();
|
||||
}
|
19
src/NadekoBot/Modules/Games/Fish/FishConfigService.cs
Normal file
19
src/NadekoBot/Modules/Games/Fish/FishConfigService.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using NadekoBot.Common.Configs;
|
||||
|
||||
namespace NadekoBot.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)
|
||||
{
|
||||
}
|
||||
}
|
16
src/NadekoBot/Modules/Games/Fish/FishData.cs
Normal file
16
src/NadekoBot/Modules/Games/Fish/FishData.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace NadekoBot.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; }
|
||||
}
|
9
src/NadekoBot/Modules/Games/Fish/FishResult.cs
Normal file
9
src/NadekoBot/Modules/Games/Fish/FishResult.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace NadekoBot.Modules.Games;
|
||||
|
||||
public sealed class FishResult
|
||||
{
|
||||
public required FishData Fish { get; init; }
|
||||
public int Stars { get; init; }
|
||||
}
|
||||
|
||||
public readonly record struct AlreadyFishing;
|
318
src/NadekoBot/Modules/Games/Fish/FishService.cs
Normal file
318
src/NadekoBot/Modules/Games/Fish/FishService.cs
Normal file
@@ -0,0 +1,318 @@
|
||||
using LinqToDB;
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace NadekoBot.Modules.Games;
|
||||
|
||||
public sealed class FishService(FishConfigService fcs, IBotCache cache, DbService db) : INService
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user