mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-10 09:18:27 -04:00
Compare commits
21 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
e40c9335c1 | ||
|
d921b6889d | ||
|
aaef365bdc | ||
|
a01a646cbf | ||
|
5bee5e63d2 | ||
|
815e318610 | ||
|
634c6c99ee | ||
|
6b37b49439 | ||
|
f42afa7eae | ||
|
ccae1c59e9 | ||
|
b0d8137a7a | ||
|
e78a7d0efa | ||
|
1da19a51f6 | ||
|
91eed9dbd8 | ||
|
7ba345b0fc | ||
|
5d775c9589 | ||
|
2bd8ead10c | ||
|
1148ba3e6e | ||
|
5498bec8cc | ||
|
9eed0c6be5 | ||
|
acf6b7cf58 |
67
CHANGELOG.md
67
CHANGELOG.md
@@ -2,6 +2,73 @@
|
||||
|
||||
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
|
||||
|
||||
- .sar rm will now accept role ids in case the role was deleted
|
||||
- `.deletewaifus` should work again
|
||||
|
||||
## [5.3.4] - 14.01.2025
|
||||
|
||||
## Added
|
||||
|
||||
- Added `.fish` commands
|
||||
- `.fish` - Attempt to catch a fish - different fish live in different places, at different times and during different times of the day
|
||||
- `.fishlist` - 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
|
||||
- `.fishspot` - Shows information about the current fish spot, time of day and weather
|
||||
|
||||
## Fixed
|
||||
|
||||
- `.timely` fixed captcha sometimes generating only 2 characters
|
||||
|
||||
## [5.3.3] - 15.12.2024
|
||||
|
||||
## Fixed
|
||||
|
||||
- `.notify` commands are no longer owner only, they now require Admin permissions
|
||||
- `.notify` messages can now mention anyone
|
||||
|
||||
## [5.3.2] - 14.12.2024
|
||||
|
||||
## Fixed
|
||||
|
||||
- `.banner` should be working properly now with both server and global user banners
|
||||
|
||||
## [5.3.1] - 13.12.2024
|
||||
|
||||
## Changed
|
||||
|
||||
- `.translate` will now use 2 embeds, to allow for longer messages
|
||||
- Added role icon to `.inrole`, if it exists
|
||||
- `.honeypot` will now add a 'Honeypot' as a ban reason.
|
||||
|
||||
## Fixed
|
||||
|
||||
- `.winlb` looks better, has a title, shows 9 entries now
|
||||
- `.sar ex` help updated
|
||||
- `.banner` partially fixed, it still can't show global banners, but it will show guild ones correctly, in a good enough size
|
||||
- `.sclr` will now show correct color hexes without alpha
|
||||
- `.dmcmd` will now correctly block commands in dms, not globally
|
||||
|
||||
## [5.3.0] - 10.12.2024
|
||||
|
||||
## Added
|
||||
|
88
src/NadekoBot.Tests/FishTests.cs
Normal file
88
src/NadekoBot.Tests/FishTests.cs
Normal file
@@ -0,0 +1,88 @@
|
||||
// using System;
|
||||
// using System.Collections.Generic;
|
||||
// using System.Diagnostics;
|
||||
// using System.IO;
|
||||
// using System.Linq;
|
||||
// using Nadeko.Common;
|
||||
// using NadekoBot.Modules.Games;
|
||||
// using NUnit.Framework;
|
||||
//
|
||||
// namespace NadekoBot.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;
|
||||
// }
|
||||
// }
|
@@ -1,4 +1,5 @@
|
||||
using Nadeko.Common;
|
||||
using System;
|
||||
using Nadeko.Common;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace NadekoBot.Tests
|
||||
@@ -120,5 +121,12 @@ namespace NadekoBot.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());
|
||||
}
|
||||
}
|
||||
}
|
@@ -218,12 +218,12 @@ public sealed class Bot : IBot
|
||||
catch (HttpException ex)
|
||||
{
|
||||
LoginErrorHandler.Handle(ex);
|
||||
Helpers.ReadErrorAndExit(3);
|
||||
Helpers.ReadErrorAndExit(101);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoginErrorHandler.Handle(ex);
|
||||
Helpers.ReadErrorAndExit(4);
|
||||
Helpers.ReadErrorAndExit(5);
|
||||
}
|
||||
|
||||
await clientReady.Task.ConfigureAwait(false);
|
||||
@@ -275,7 +275,7 @@ public sealed class Bot : IBot
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Error adding services");
|
||||
Helpers.ReadErrorAndExit(9);
|
||||
Helpers.ReadErrorAndExit(103);
|
||||
}
|
||||
|
||||
Log.Information("Shard {ShardId} connected in {Elapsed:F2}s",
|
||||
|
@@ -74,6 +74,9 @@ public abstract class NadekoContext : DbContext
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
// load all entities from current assembly
|
||||
modelBuilder.ApplyConfigurationsFromAssembly(typeof(NadekoContext).Assembly);
|
||||
|
||||
#region Notify
|
||||
|
||||
modelBuilder.Entity<Notify>(e =>
|
||||
|
@@ -17,6 +17,9 @@ public static class MigrationQueries
|
||||
|
||||
public static void MigrateSar(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
if (migrationBuilder.IsNpgsql())
|
||||
return;
|
||||
|
||||
migrationBuilder.Sql("""
|
||||
INSERT INTO GroupName (Number, GuildConfigId)
|
||||
SELECT DISTINCT "Group", GC.Id
|
||||
|
4151
src/NadekoBot/Migrations/PostgreSql/20250113135504_fishes.Designer.cs
generated
Normal file
4151
src/NadekoBot/Migrations/PostgreSql/20250113135504_fishes.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
39
src/NadekoBot/Migrations/PostgreSql/20250113135504_fishes.cs
Normal file
39
src/NadekoBot/Migrations/PostgreSql/20250113135504_fishes.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace NadekoBot.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");
|
||||
}
|
||||
}
|
||||
}
|
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");
|
||||
}
|
||||
}
|
||||
}
|
@@ -3374,6 +3374,67 @@ namespace NadekoBot.Migrations.PostgreSql
|
||||
b.ToTable("xpshopowneditem", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NadekoBot.Modules.Games.FishCatch", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("Count")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("count");
|
||||
|
||||
b.Property<int>("FishId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("fishid");
|
||||
|
||||
b.Property<int>("MaxStars")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("maxstars");
|
||||
|
||||
b.Property<decimal>("UserId")
|
||||
.HasColumnType("numeric(20,0)")
|
||||
.HasColumnName("userid");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_fishcatch");
|
||||
|
||||
b.HasAlternateKey("UserId", "FishId")
|
||||
.HasName("ak_fishcatch_userid_fishid");
|
||||
|
||||
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")
|
||||
|
3198
src/NadekoBot/Migrations/Sqlite/20250113135453_fishes.Designer.cs
generated
Normal file
3198
src/NadekoBot/Migrations/Sqlite/20250113135453_fishes.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
38
src/NadekoBot/Migrations/Sqlite/20250113135453_fishes.cs
Normal file
38
src/NadekoBot/Migrations/Sqlite/20250113135453_fishes.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace NadekoBot.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");
|
||||
}
|
||||
}
|
||||
}
|
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");
|
||||
}
|
||||
}
|
||||
}
|
@@ -2508,6 +2508,51 @@ namespace NadekoBot.Migrations
|
||||
b.ToTable("XpShopOwnedItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NadekoBot.Modules.Games.FishCatch", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Count")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("FishId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("MaxStars")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<ulong>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasAlternateKey("UserId", "FishId");
|
||||
|
||||
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")
|
||||
|
@@ -8,7 +8,7 @@ public partial class Administration
|
||||
public class NotifyCommands : NadekoModule<NotifyService>
|
||||
{
|
||||
[Cmd]
|
||||
[OwnerOnly]
|
||||
[UserPerm(GuildPerm.Administrator)]
|
||||
public async Task Notify()
|
||||
{
|
||||
await Response()
|
||||
@@ -42,7 +42,7 @@ public partial class Administration
|
||||
};
|
||||
|
||||
[Cmd]
|
||||
[OwnerOnly]
|
||||
[UserPerm(GuildPerm.Administrator)]
|
||||
public async Task Notify(NotifyType nType, [Leftover] string? message = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(message))
|
||||
@@ -76,7 +76,7 @@ public partial class Administration
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[OwnerOnly]
|
||||
[UserPerm(GuildPerm.Administrator)]
|
||||
public async Task NotifyList(int page = 1)
|
||||
{
|
||||
if (--page < 0)
|
||||
@@ -104,7 +104,7 @@ public partial class Administration
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[OwnerOnly]
|
||||
[UserPerm(GuildPerm.Administrator)]
|
||||
public async Task NotifyClear(NotifyType nType)
|
||||
{
|
||||
await _service.DisableAsync(ctx.Guild.Id, nType);
|
||||
|
@@ -75,7 +75,7 @@ public sealed class NotifyService : IReadyExecutor, INotifySubscriber, INService
|
||||
data);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async Task OnEvent<T>(T model)
|
||||
where T : struct, INotifyModel
|
||||
{
|
||||
@@ -146,6 +146,7 @@ public sealed class NotifyService : IReadyExecutor, INotifySubscriber, INService
|
||||
|
||||
await _mss.Response(channel)
|
||||
.Text(st)
|
||||
.Sanitize(false)
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
|
@@ -192,15 +192,22 @@ public partial class Administration
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.ManageRoles)]
|
||||
public async Task SarRemove([Leftover] IRole role)
|
||||
[Priority(1)]
|
||||
public Task SarRemove([Leftover] IRole role)
|
||||
=> SarRemove(role.Id);
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.ManageRoles)]
|
||||
[Priority(0)]
|
||||
public async Task SarRemove([Leftover] ulong roleId)
|
||||
{
|
||||
var guser = (IGuildUser)ctx.User;
|
||||
|
||||
var success = await _service.RemoveAsync(role.Guild.Id, role.Id);
|
||||
var role = await ctx.Guild.GetRoleAsync(roleId);
|
||||
var success = await _service.RemoveAsync(ctx.Guild.Id, roleId);
|
||||
if (!success)
|
||||
await Response().Error(strs.self_assign_not).SendAsync();
|
||||
else
|
||||
await Response().Confirm(strs.self_assign_rem(Format.Bold(role.Name))).SendAsync();
|
||||
await Response().Confirm(strs.self_assign_rem(Format.Bold(role?.Name ?? roleId.ToString()))).SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
|
@@ -59,10 +59,15 @@ public class SelfAssignedRolesService : INService, IReadyExecutor
|
||||
},
|
||||
_ => new()
|
||||
{
|
||||
SarGroupId = ctx.GetTable<SarGroup>()
|
||||
.Where(x => x.GuildId == guildId && x.GroupNumber == groupNumber)
|
||||
.Select(x => x.Id)
|
||||
.First()
|
||||
},
|
||||
() => new()
|
||||
{
|
||||
RoleId = roleId,
|
||||
GuildId = guildId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -280,8 +285,12 @@ public sealed class SarAssignerService : INService, IReadyExecutor
|
||||
|
||||
if (item.Group.IsExclusive)
|
||||
{
|
||||
var rolesToRemove = item.Group.Roles.Select(x => x.RoleId);
|
||||
await item.User.RemoveRolesAsync(rolesToRemove);
|
||||
var rolesToRemove = item.Group.Roles
|
||||
.Where(x => item.User.RoleIds.Contains(x.RoleId))
|
||||
.Select(x => x.RoleId)
|
||||
.ToArray();
|
||||
if (rolesToRemove.Length > 0)
|
||||
await item.User.RemoveRolesAsync(rolesToRemove);
|
||||
}
|
||||
|
||||
await item.User.AddRoleAsync(item.RoleId);
|
||||
|
@@ -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);
|
||||
|
@@ -16,9 +16,9 @@ public class GamblingCleanupService : IGamblingCleanupService, INService
|
||||
public async Task DeleteWaifus()
|
||||
{
|
||||
await using var ctx = _db.GetDbContext();
|
||||
await ctx.GetTable<WaifuInfo>().DeleteAsync();
|
||||
await ctx.GetTable<WaifuItem>().DeleteAsync();
|
||||
await ctx.GetTable<WaifuUpdate>().DeleteAsync();
|
||||
await ctx.GetTable<WaifuInfo>().DeleteAsync();
|
||||
}
|
||||
|
||||
public async Task DeleteWaifu(ulong userId)
|
||||
|
80
src/NadekoBot/Modules/Games/Fish/CaptchaService.cs
Normal file
80
src/NadekoBot/Modules/Games/Fish/CaptchaService.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
using NadekoBot.Db.Models;
|
||||
using NadekoBot.Modules.Patronage;
|
||||
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, IBotCache cache, IPatronageService ps) : 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();
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
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;
|
||||
}
|
275
src/NadekoBot/Modules/Games/Fish/FishCommands.cs
Normal file
275
src/NadekoBot/Modules/Games/Fish/FishCommands.cs
Normal file
@@ -0,0 +1,275 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
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 captchaService) : NadekoModule
|
||||
{
|
||||
private static readonly NadekoRandom _rng = new();
|
||||
|
||||
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 captchaService.GetUserCaptcha(ctx.User.Id);
|
||||
if (password is not null)
|
||||
{
|
||||
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
|
||||
{
|
||||
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 captchaService.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;
|
||||
}
|
||||
|
||||
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(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))
|
||||
.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();
|
||||
|
||||
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);
|
||||
|
||||
await Response()
|
||||
.Paginated()
|
||||
.Items(fishes)
|
||||
.PageSize(9)
|
||||
.CurrentPage(page)
|
||||
.Page((fs, i) =>
|
||||
{
|
||||
var eb = CreateEmbed()
|
||||
.WithDescription($"🧠 **Skill:** {skill} / {maxSkill}")
|
||||
.WithAuthor(ctx.User)
|
||||
.WithTitle(GetText(strs.fish_list_title))
|
||||
.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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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; }
|
||||
}
|
12
src/NadekoBot/Modules/Games/Fish/FishResult.cs
Normal file
12
src/NadekoBot/Modules/Games/Fish/FishResult.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace NadekoBot.Modules.Games;
|
||||
|
||||
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;
|
||||
|
421
src/NadekoBot/Modules/Games/Fish/FishService.cs
Normal file
421
src/NadekoBot/Modules/Games/Fish/FishService.cs
Normal file
@@ -0,0 +1,421 @@
|
||||
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)
|
||||
=> new($"fishing:{userId}");
|
||||
|
||||
public async Task<OneOf.OneOf<Task<FishResult?>, AlreadyFishing>> FishAsync(ulong userId, ulong channelId)
|
||||
{
|
||||
var duration = _rng.Next(5, 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));
|
||||
|
||||
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 = fishChance + trashChance + conf.Chance.Nothing;
|
||||
|
||||
var typeRoll = _rng.NextDouble() * totalChance;
|
||||
|
||||
if (typeRoll < nothingChance)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var items = typeRoll < nothingChance + fishChance
|
||||
? conf.Fish
|
||||
: conf.Trash;
|
||||
|
||||
|
||||
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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
var maxSum = filteredItems.Sum(x => x.Chance * 100);
|
||||
|
||||
|
||||
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,
|
||||
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 >> 29) % 10;
|
||||
|
||||
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)
|
||||
{
|
||||
// 15% chance of 1 star, 85% chance of 2 stars
|
||||
return _rng.NextDouble() < 0.85 ? 1 : 2;
|
||||
}
|
||||
|
||||
if (maxStars == 3)
|
||||
{
|
||||
// 65% chance of 1 star, 30% chance of 2 stars, 5% chance of 3 stars
|
||||
var r = _rng.NextDouble();
|
||||
if (r < 0.65)
|
||||
return 1;
|
||||
if (r < 0.95)
|
||||
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.55)
|
||||
return 1;
|
||||
if (r < 0.80)
|
||||
return 2;
|
||||
if (r < 0.98)
|
||||
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.98)
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
@@ -417,7 +417,8 @@ public partial class Searches : NadekoModule<SearchesService>
|
||||
{
|
||||
usr ??= (IGuildUser)ctx.User;
|
||||
|
||||
var bannerUrl = usr.GetGuildBannerUrl(size: 2048);
|
||||
var bannerUrl = usr.GetGuildBannerUrl(size: 2048)
|
||||
?? (await ((DiscordSocketClient)ctx.Client).Rest.GetUserAsync(usr.Id))?.GetBannerUrl();
|
||||
|
||||
if (bannerUrl is null)
|
||||
{
|
||||
|
@@ -4,7 +4,7 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>true</ImplicitUsings>
|
||||
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||
<Version>5.3.0</Version>
|
||||
<Version>5.3.6</Version>
|
||||
|
||||
<!-- Output/build -->
|
||||
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>
|
||||
|
@@ -16,7 +16,7 @@ public sealed class NadekoRandom : Random
|
||||
_rng.GetBytes(bytes);
|
||||
return Math.Abs(BitConverter.ToInt32(bytes, 0));
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Generates a random integer between 0 (inclusive) and
|
||||
/// a specified exclusive upper bound using a cryptographically strong random number generator.
|
||||
@@ -54,13 +54,9 @@ public sealed class NadekoRandom : 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();
|
||||
}
|
@@ -82,7 +82,7 @@ public sealed class BotCredsProvider : IBotCredsProvider
|
||||
if (string.IsNullOrWhiteSpace(_creds.Token))
|
||||
{
|
||||
Log.Error("Token is missing from creds.yml or Environment variables.\nAdd it and restart the program");
|
||||
Helpers.ReadErrorAndExit(5);
|
||||
Helpers.ReadErrorAndExit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@@ -107,7 +107,7 @@ public class RemoteGrpcCoordinator : ICoordinator, IReadyExecutor
|
||||
await Task.Delay(7500);
|
||||
}
|
||||
|
||||
Environment.Exit(5);
|
||||
Environment.Exit(0);
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
|
@@ -33,7 +33,7 @@ public class SingleProcessCoordinator : ICoordinator
|
||||
}
|
||||
|
||||
public void Die(bool graceful = false)
|
||||
=> Environment.Exit(5);
|
||||
=> Environment.Exit(0);
|
||||
|
||||
public bool RestartShard(int shardId)
|
||||
=> RestartBot();
|
||||
|
@@ -1559,4 +1559,17 @@ notifyclear:
|
||||
- notifclr
|
||||
winlb:
|
||||
- winlb
|
||||
- wins
|
||||
- wins
|
||||
fish:
|
||||
- fish
|
||||
- fi
|
||||
fishlist:
|
||||
- fishlist
|
||||
- fili
|
||||
- fishes
|
||||
- fil
|
||||
- fishlist
|
||||
fishspot:
|
||||
- fishspot
|
||||
- fisp
|
||||
- fish?
|
43
src/NadekoBot/data/fish.yml
Normal file
43
src/NadekoBot/data/fish.yml
Normal 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>"
|
@@ -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 of day and in different weather.
|
||||
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:
|
||||
- { }
|
@@ -1159,5 +1159,17 @@
|
||||
"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",
|
||||
"fish_skill_up": "Fishing skill increased to **{0} / {1}**",
|
||||
"fish_list_title": "Fishing"
|
||||
}
|
||||
|
Reference in New Issue
Block a user