2025-02-04 01:35:35 +13:00
|
|
|
|
using System.Security.Cryptography;
|
2025-03-24 14:06:49 +13:00
|
|
|
|
using System.Text;
|
2025-03-30 13:48:14 +13:00
|
|
|
|
using AngleSharp.Common;
|
2025-02-04 01:35:35 +13:00
|
|
|
|
using LinqToDB;
|
2025-01-14 19:03:59 +13:00
|
|
|
|
using LinqToDB.EntityFrameworkCore;
|
2025-03-24 14:06:49 +13:00
|
|
|
|
using EllieBot.Modules.Administration;
|
|
|
|
|
using EllieBot.Modules.Administration.Services;
|
2025-03-28 21:13:53 +13:00
|
|
|
|
using EllieBot.Modules.Games.Quests;
|
2025-01-14 19:03:59 +13:00
|
|
|
|
|
2025-02-04 01:35:35 +13:00
|
|
|
|
namespace EllieBot.Modules.Games.Fish;
|
2025-01-14 19:03:59 +13:00
|
|
|
|
|
2025-03-24 14:06:49 +13:00
|
|
|
|
public sealed class FishService(
|
|
|
|
|
FishConfigService fcs,
|
|
|
|
|
IBotCache cache,
|
|
|
|
|
DbService db,
|
2025-03-28 21:13:53 +13:00
|
|
|
|
INotifySubscriber notify,
|
2025-03-29 20:33:25 +13:00
|
|
|
|
QuestService quests,
|
|
|
|
|
FishItemService itemService
|
2025-03-24 14:06:49 +13:00
|
|
|
|
)
|
|
|
|
|
: IEService
|
2025-01-14 19:03:59 +13:00
|
|
|
|
{
|
2025-02-04 01:35:35 +13:00
|
|
|
|
private const double MAX_SKILL = 100;
|
2025-01-21 16:33:39 +13:00
|
|
|
|
|
2025-02-04 01:35:35 +13:00
|
|
|
|
private readonly Random _rng = new Random();
|
2025-01-14 19:03:59 +13:00
|
|
|
|
|
|
|
|
|
private static TypedKey<bool> FishingKey(ulong userId)
|
|
|
|
|
=> new($"fishing:{userId}");
|
|
|
|
|
|
2025-03-29 20:33:25 +13:00
|
|
|
|
public async Task<OneOf.OneOf<Task<FishResult?>, AlreadyFishing>> FishAsync(ulong userId, ulong channelId,
|
|
|
|
|
FishMultipliers multipliers)
|
2025-01-14 19:03:59 +13:00
|
|
|
|
{
|
2025-03-29 20:33:25 +13:00
|
|
|
|
var duration = _rng.Next(3, 6) / multipliers.FishingSpeedMultiplier;
|
2025-01-14 19:03:59 +13:00
|
|
|
|
|
|
|
|
|
if (!await cache.AddAsync(FishingKey(userId), true, TimeSpan.FromSeconds(duration), overwrite: false))
|
|
|
|
|
{
|
|
|
|
|
return new AlreadyFishing();
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-29 20:33:25 +13:00
|
|
|
|
return TryFishAsync(userId, channelId, duration, multipliers);
|
2025-01-14 19:03:59 +13:00
|
|
|
|
}
|
|
|
|
|
|
2025-03-29 20:33:25 +13:00
|
|
|
|
private async Task<FishResult?> TryFishAsync(
|
|
|
|
|
ulong userId,
|
|
|
|
|
ulong channelId,
|
|
|
|
|
double duration,
|
|
|
|
|
FishMultipliers multipliers)
|
2025-01-14 19:03:59 +13:00
|
|
|
|
{
|
|
|
|
|
var conf = fcs.Data;
|
|
|
|
|
await Task.Delay(TimeSpan.FromSeconds(duration));
|
|
|
|
|
|
2025-01-21 16:33:39 +13:00
|
|
|
|
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;
|
2025-03-29 20:33:25 +13:00
|
|
|
|
var fishChance = conf.Chance.Fish * fishChanceMultiplier * multipliers.FishMultiplier;
|
|
|
|
|
var trashChance = conf.Chance.Trash * trashChanceMultiplier * multipliers.TrashMultiplier;
|
2025-01-21 16:33:39 +13:00
|
|
|
|
|
2025-01-14 19:03:59 +13:00
|
|
|
|
// first roll whether it's fish, trash or nothing
|
2025-01-21 16:33:39 +13:00
|
|
|
|
var totalChance = fishChance + trashChance + conf.Chance.Nothing;
|
|
|
|
|
|
2025-01-14 19:03:59 +13:00
|
|
|
|
var typeRoll = _rng.NextDouble() * totalChance;
|
|
|
|
|
|
2025-01-21 16:33:39 +13:00
|
|
|
|
if (typeRoll < nothingChance)
|
2025-01-14 19:03:59 +13:00
|
|
|
|
{
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-29 20:33:25 +13:00
|
|
|
|
var isFish = typeRoll < nothingChance + fishChance;
|
|
|
|
|
|
|
|
|
|
var items = isFish
|
2025-01-14 19:03:59 +13:00
|
|
|
|
? conf.Fish
|
|
|
|
|
: conf.Trash;
|
|
|
|
|
|
2025-03-29 20:33:25 +13:00
|
|
|
|
var result = await FishAsyncInternal(userId, channelId, items, multipliers);
|
|
|
|
|
|
|
|
|
|
// use bait
|
|
|
|
|
if (result is not null)
|
|
|
|
|
{
|
|
|
|
|
await itemService.UseBaitAsync(userId);
|
|
|
|
|
}
|
2025-01-21 16:33:39 +13:00
|
|
|
|
|
2025-03-29 20:33:25 +13:00
|
|
|
|
// skill
|
2025-01-21 16:33:39 +13:00
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-24 14:06:49 +13:00
|
|
|
|
// notification system
|
|
|
|
|
if (result is not null)
|
|
|
|
|
{
|
|
|
|
|
if (result.IsMaxStar() || result.IsRare())
|
|
|
|
|
{
|
|
|
|
|
await notify.NotifyAsync(new NiceCatchNotifyModel(
|
|
|
|
|
userId,
|
|
|
|
|
result.Fish,
|
|
|
|
|
GetStarText(result.Stars, result.Fish.Stars)
|
|
|
|
|
));
|
|
|
|
|
}
|
2025-01-21 16:33:39 +13:00
|
|
|
|
|
2025-03-29 20:33:25 +13:00
|
|
|
|
await quests.ReportActionAsync(userId,
|
|
|
|
|
QuestEventType.FishCaught,
|
|
|
|
|
new()
|
|
|
|
|
{
|
|
|
|
|
{ "fish", result.Fish.Name },
|
|
|
|
|
{ "type", typeRoll < nothingChance + fishChance ? "fish" : "trash" },
|
|
|
|
|
{ "stars", result.Stars.ToString() }
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-03-28 21:13:53 +13:00
|
|
|
|
|
2025-01-21 16:33:39 +13:00
|
|
|
|
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>()
|
2025-03-24 14:06:49 +13:00
|
|
|
|
.InsertOrUpdateAsync(() => new()
|
2025-03-29 20:33:25 +13:00
|
|
|
|
{
|
|
|
|
|
UserId = userId,
|
|
|
|
|
Skill = 1,
|
|
|
|
|
},
|
|
|
|
|
(old) => new()
|
|
|
|
|
{
|
|
|
|
|
UserId = userId,
|
|
|
|
|
Skill = old.Skill > maxSkill ? maxSkill : old.Skill + 1
|
|
|
|
|
},
|
|
|
|
|
() => new()
|
|
|
|
|
{
|
|
|
|
|
UserId = userId,
|
|
|
|
|
Skill = playerSkill
|
|
|
|
|
});
|
2025-01-21 16:33:39 +13:00
|
|
|
|
|
|
|
|
|
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>()
|
2025-03-24 14:06:49 +13:00
|
|
|
|
.Where(x => x.UserId == userId)
|
|
|
|
|
.Select(x => x.Skill)
|
|
|
|
|
.FirstOrDefaultAsyncLinqToDB();
|
2025-01-21 16:33:39 +13:00
|
|
|
|
|
|
|
|
|
return (skill, (int)MAX_SKILL);
|
2025-01-14 19:03:59 +13:00
|
|
|
|
}
|
|
|
|
|
|
2025-03-29 20:33:25 +13:00
|
|
|
|
private async Task<FishResult?> FishAsyncInternal(
|
|
|
|
|
ulong userId,
|
|
|
|
|
ulong channelId,
|
|
|
|
|
List<FishData> items,
|
|
|
|
|
FishMultipliers multipliers)
|
2025-01-14 19:03:59 +13:00
|
|
|
|
{
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-29 20:33:25 +13:00
|
|
|
|
|
|
|
|
|
var maxSum = filteredItems
|
|
|
|
|
.Select(x => (x.Id, x.Chance, x.Stars))
|
|
|
|
|
.Select(x =>
|
|
|
|
|
{
|
|
|
|
|
if (x.Chance <= 15)
|
|
|
|
|
return x with
|
|
|
|
|
{
|
|
|
|
|
Chance = x.Chance *= multipliers.RareMultiplier
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return x;
|
|
|
|
|
})
|
|
|
|
|
.Sum(x => { return x.Chance * 100; });
|
2025-01-14 19:03:59 +13:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var roll = _rng.NextDouble() * maxSum;
|
|
|
|
|
|
|
|
|
|
FishResult? caught = null;
|
|
|
|
|
|
|
|
|
|
var curSum = 0d;
|
|
|
|
|
foreach (var i in filteredItems)
|
|
|
|
|
{
|
|
|
|
|
curSum += i.Chance * 100;
|
|
|
|
|
|
|
|
|
|
if (roll < curSum)
|
|
|
|
|
{
|
|
|
|
|
caught = new FishResult()
|
|
|
|
|
{
|
|
|
|
|
Fish = i,
|
2025-03-29 20:33:25 +13:00
|
|
|
|
Stars = GetRandomStars(i.Stars, multipliers),
|
2025-01-14 19:03:59 +13:00
|
|
|
|
};
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (caught is not null)
|
|
|
|
|
{
|
|
|
|
|
await using var uow = db.GetDbContext();
|
|
|
|
|
|
|
|
|
|
await uow.GetTable<FishCatch>()
|
2025-03-24 14:06:49 +13:00
|
|
|
|
.InsertOrUpdateAsync(() => new FishCatch()
|
2025-03-29 20:33:25 +13:00
|
|
|
|
{
|
|
|
|
|
UserId = userId,
|
|
|
|
|
FishId = caught.Fish.Id,
|
|
|
|
|
MaxStars = caught.Stars,
|
|
|
|
|
Count = 1
|
|
|
|
|
},
|
|
|
|
|
(old) => new FishCatch()
|
|
|
|
|
{
|
|
|
|
|
Count = old.Count + 1,
|
|
|
|
|
MaxStars = Math.Max(old.MaxStars, caught.Stars),
|
|
|
|
|
},
|
|
|
|
|
() => new()
|
|
|
|
|
{
|
|
|
|
|
FishId = caught.Fish.Id,
|
|
|
|
|
UserId = userId
|
|
|
|
|
});
|
2025-01-14 19:03:59 +13:00
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
{
|
2025-01-21 16:33:39 +13:00
|
|
|
|
var cid = (channelId >> 22 >> 29) % 10;
|
2025-01-14 19:03:59 +13:00
|
|
|
|
|
|
|
|
|
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>
|
2025-03-29 20:33:25 +13:00
|
|
|
|
/// <param name="multipliers"></param>
|
2025-01-14 19:03:59 +13:00
|
|
|
|
/// <returns>Random number of stars</returns>
|
2025-03-29 20:33:25 +13:00
|
|
|
|
private int GetRandomStars(int maxStars, FishMultipliers multipliers)
|
2025-01-14 19:03:59 +13:00
|
|
|
|
{
|
|
|
|
|
if (maxStars == 1)
|
|
|
|
|
return 1;
|
|
|
|
|
|
2025-03-29 20:33:25 +13:00
|
|
|
|
var maxStarMulti = multipliers.StarMultiplier;
|
|
|
|
|
double baseChance;
|
2025-01-14 19:03:59 +13:00
|
|
|
|
if (maxStars == 2)
|
|
|
|
|
{
|
2025-01-15 19:56:46 +13:00
|
|
|
|
// 15% chance of 1 star, 85% chance of 2 stars
|
2025-03-29 20:33:25 +13:00
|
|
|
|
baseChance = Math.Clamp(0.15 * multipliers.StarMultiplier, 0, 1);
|
|
|
|
|
return _rng.NextDouble() < (1 - baseChance) ? 1 : 2;
|
2025-01-14 19:03:59 +13:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (maxStars == 3)
|
|
|
|
|
{
|
2025-01-15 19:56:46 +13:00
|
|
|
|
// 65% chance of 1 star, 30% chance of 2 stars, 5% chance of 3 stars
|
2025-03-29 20:33:25 +13:00
|
|
|
|
baseChance = 0.05 * multipliers.StarMultiplier;
|
2025-01-14 19:03:59 +13:00
|
|
|
|
var r = _rng.NextDouble();
|
2025-03-29 20:33:25 +13:00
|
|
|
|
if (r < (1 - baseChance - 0.3))
|
2025-01-14 19:03:59 +13:00
|
|
|
|
return 1;
|
2025-03-29 20:33:25 +13:00
|
|
|
|
if (r < (1 - baseChance))
|
2025-01-14 19:03:59 +13:00
|
|
|
|
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();
|
2025-03-29 20:33:25 +13:00
|
|
|
|
baseChance = 0.02 * multipliers.StarMultiplier;
|
|
|
|
|
if (r < (1 - baseChance - 0.45))
|
2025-01-14 19:03:59 +13:00
|
|
|
|
return 1;
|
2025-03-29 20:33:25 +13:00
|
|
|
|
if (r < (1 - baseChance - 0.15))
|
2025-01-14 19:03:59 +13:00
|
|
|
|
return 2;
|
2025-03-29 20:33:25 +13:00
|
|
|
|
if (r < (1 - baseChance))
|
2025-01-14 19:03:59 +13:00
|
|
|
|
return 3;
|
|
|
|
|
return 4;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (maxStars == 5)
|
|
|
|
|
{
|
2025-03-29 20:33:25 +13:00
|
|
|
|
// 40% chance of 1 star, 30% chance of 2 stars, 15% chance of 3 stars, 10% chance of 4 stars, 2% chance of 5 stars
|
2025-01-14 19:03:59 +13:00
|
|
|
|
var r = _rng.NextDouble();
|
2025-03-29 20:33:25 +13:00
|
|
|
|
baseChance = 0.02 * multipliers.StarMultiplier;
|
|
|
|
|
if (r < (1 - baseChance - 0.6))
|
2025-01-14 19:03:59 +13:00
|
|
|
|
return 1;
|
2025-03-29 20:33:25 +13:00
|
|
|
|
if (r < (1 - baseChance - 0.3))
|
2025-01-14 19:03:59 +13:00
|
|
|
|
return 2;
|
2025-03-29 20:33:25 +13:00
|
|
|
|
if (r < (1 - baseChance - 0.1))
|
2025-01-14 19:03:59 +13:00
|
|
|
|
return 3;
|
2025-03-29 20:33:25 +13:00
|
|
|
|
if (r < (1 - baseChance))
|
2025-01-14 19:03:59 +13:00
|
|
|
|
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>()
|
2025-03-24 14:06:49 +13:00
|
|
|
|
.Where(x => x.UserId == userId)
|
|
|
|
|
.ToListAsyncLinqToDB();
|
2025-01-14 19:03:59 +13:00
|
|
|
|
|
|
|
|
|
return catches;
|
|
|
|
|
}
|
2025-03-24 14:06:49 +13:00
|
|
|
|
|
2025-04-01 13:24:46 +13:00
|
|
|
|
public async Task<IReadOnlyCollection<(ulong UserId, int Catches, int Unique)>> GetFishLbAsync(int page)
|
2025-03-30 13:48:14 +13:00
|
|
|
|
{
|
|
|
|
|
await using var ctx = db.GetDbContext();
|
|
|
|
|
|
|
|
|
|
var result = await ctx.GetTable<FishCatch>()
|
|
|
|
|
.GroupBy(x => x.UserId)
|
2025-04-01 13:26:17 +13:00
|
|
|
|
.OrderByDescending(x => x.Count()).ThenByDescending(x => x.Sum(x => x.Count))
|
2025-03-30 13:48:14 +13:00
|
|
|
|
.Skip(page * 10)
|
|
|
|
|
.Take(10)
|
|
|
|
|
.Select(x => new
|
|
|
|
|
{
|
|
|
|
|
UserId = x.Key,
|
2025-04-01 13:24:46 +13:00
|
|
|
|
Catches = x.Sum(x => x.Count),
|
|
|
|
|
Unique = x.Count()
|
2025-03-30 13:48:14 +13:00
|
|
|
|
})
|
|
|
|
|
.ToListAsyncLinqToDB()
|
2025-04-01 13:24:46 +13:00
|
|
|
|
.Fmap(x => x.Map(y => (y.UserId, y.Catches, y.Unique)).ToList());
|
2025-03-30 13:48:14 +13:00
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-24 14:06:49 +13:00
|
|
|
|
public 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();
|
|
|
|
|
}
|
2025-03-30 13:48:14 +13:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public sealed class IUserFishCatch
|
|
|
|
|
{
|
|
|
|
|
public ulong UserId { get; set; }
|
|
|
|
|
public int Count { get; set; }
|
2025-01-21 16:33:39 +13:00
|
|
|
|
}
|