Added .fish command
This commit is contained in:
parent
c574956d94
commit
b71e8969a0
23 changed files with 15047 additions and 6612 deletions
88
src/EllieBot.Tests/FishTests.cs
Normal file
88
src/EllieBot.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 Ellie.Common;
|
||||||
|
// using EllieBot.Modules.Games;
|
||||||
|
// using NUnit.Framework;
|
||||||
|
//
|
||||||
|
// namespace EllieBot.Tests;
|
||||||
|
//
|
||||||
|
// public class FishTests
|
||||||
|
// {
|
||||||
|
// [Test]
|
||||||
|
// public void TestWeather()
|
||||||
|
// {
|
||||||
|
// var fs = new FishService(null, null);
|
||||||
|
//
|
||||||
|
// var rng = new Random();
|
||||||
|
//
|
||||||
|
// // output = @"ro+dD:bN0uVqV3ZOAv6r""EFeA'A]u]uSyz2Qd'r#0Vf:5zOX\VgSsF8LgRCL/uOW";
|
||||||
|
// while (true)
|
||||||
|
// {
|
||||||
|
// var output = "";
|
||||||
|
// for (var i = 0; i < 64; i++)
|
||||||
|
// {
|
||||||
|
// var c = (char)rng.Next(33, 123);
|
||||||
|
// output += c;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// output = "";
|
||||||
|
// var weathers = new List<FishingWeather>();
|
||||||
|
// for (var i = 0; i < 1_000_000; i++)
|
||||||
|
// {
|
||||||
|
// var w = fs.GetWeather(DateTime.UtcNow.AddHours(6 * i), output);
|
||||||
|
// weathers.Add(w);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// var vals = weathers.GroupBy(x => x)
|
||||||
|
// .ToDictionary(x => x.Key, x => x.Count());
|
||||||
|
//
|
||||||
|
// var str = weathers.Select(x => (int)x).Join("");
|
||||||
|
// var maxLength = MaxLength(str);
|
||||||
|
//
|
||||||
|
// if (maxLength < 12)
|
||||||
|
// {
|
||||||
|
// foreach (var v in vals)
|
||||||
|
// {
|
||||||
|
// Console.WriteLine($"{v.Key}: {v.Value}");
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Console.WriteLine(output);
|
||||||
|
// Console.WriteLine(maxLength);
|
||||||
|
//
|
||||||
|
// File.WriteAllText("data.txt", weathers.Select(x => (int)x).Join(""));
|
||||||
|
//
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // string with same characters
|
||||||
|
// static int MaxLength(String s)
|
||||||
|
// {
|
||||||
|
// int ans = 1, temp = 1;
|
||||||
|
//
|
||||||
|
// // Traverse the string
|
||||||
|
// for (int i = 1; i < s.Length; i++)
|
||||||
|
// {
|
||||||
|
// // If character is same as
|
||||||
|
// // previous increment temp value
|
||||||
|
// if (s[i] == s[i - 1])
|
||||||
|
// {
|
||||||
|
// ++temp;
|
||||||
|
// }
|
||||||
|
// else
|
||||||
|
// {
|
||||||
|
// ans = Math.Max(ans, temp);
|
||||||
|
// temp = 1;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// ans = Math.Max(ans, temp);
|
||||||
|
//
|
||||||
|
// // Return the required answer
|
||||||
|
// return ans;
|
||||||
|
// }
|
||||||
|
// }
|
|
@ -1,4 +1,5 @@
|
||||||
using Ellie.Common;
|
using System;
|
||||||
|
using Ellie.Common;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
|
||||||
namespace EllieBot.Tests
|
namespace EllieBot.Tests
|
||||||
|
@ -120,5 +121,12 @@ namespace EllieBot.Tests
|
||||||
num = new kwum(int.MaxValue);
|
num = new kwum(int.MaxValue);
|
||||||
Assert.AreEqual("3zzzzzz", num.ToString());
|
Assert.AreEqual("3zzzzzz", num.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPower()
|
||||||
|
{
|
||||||
|
var num = new kwum((int)Math.Pow(32, 2));
|
||||||
|
Assert.AreEqual("322", num.ToString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -74,6 +74,9 @@ public abstract class EllieContext : DbContext
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
|
// load all entities from current assembly
|
||||||
|
modelBuilder.ApplyConfigurationsFromAssembly(typeof(EllieContext).Assembly);
|
||||||
|
|
||||||
#region Notify
|
#region Notify
|
||||||
|
|
||||||
modelBuilder.Entity<Notify>(e =>
|
modelBuilder.Entity<Notify>(e =>
|
||||||
|
|
4151
src/EllieBot/Migrations/PostgreSql/20250114060020_fishes.Designer.cs
generated
Normal file
4151
src/EllieBot/Migrations/PostgreSql/20250114060020_fishes.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
39
src/EllieBot/Migrations/PostgreSql/20250114060020_fishes.cs
Normal file
39
src/EllieBot/Migrations/PostgreSql/20250114060020_fishes.cs
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace EllieBot.Migrations.PostgreSql
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class fishes : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "fishcatch",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
userid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
|
||||||
|
fishid = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
count = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
maxstars = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_fishcatch", x => x.id);
|
||||||
|
table.UniqueConstraint("ak_fishcatch_userid_fishid", x => new { x.userid, x.fishid });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "fishcatch");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,9 @@
|
||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
|
using EllieBot.Db;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
using EllieBot.Db;
|
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
@ -864,6 +864,57 @@ namespace EllieBot.Migrations.PostgreSql
|
||||||
b.ToTable("discorduser", (string)null);
|
b.ToTable("discorduser", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("EllieBot.Db.Models.EllieExpression", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<bool>("AllowTarget")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasColumnName("allowtarget");
|
||||||
|
|
||||||
|
b.Property<bool>("AutoDeleteTrigger")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasColumnName("autodeletetrigger");
|
||||||
|
|
||||||
|
b.Property<bool>("ContainsAnywhere")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasColumnName("containsanywhere");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DateAdded")
|
||||||
|
.HasColumnType("timestamp without time zone")
|
||||||
|
.HasColumnName("dateadded");
|
||||||
|
|
||||||
|
b.Property<bool>("DmResponse")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasColumnName("dmresponse");
|
||||||
|
|
||||||
|
b.Property<decimal?>("GuildId")
|
||||||
|
.HasColumnType("numeric(20,0)")
|
||||||
|
.HasColumnName("guildid");
|
||||||
|
|
||||||
|
b.Property<string>("Reactions")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("reactions");
|
||||||
|
|
||||||
|
b.Property<string>("Response")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("response");
|
||||||
|
|
||||||
|
b.Property<string>("Trigger")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("trigger");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_expressions");
|
||||||
|
|
||||||
|
b.ToTable("expressions", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("EllieBot.Db.Models.ExcludedItem", b =>
|
modelBuilder.Entity("EllieBot.Db.Models.ExcludedItem", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
|
@ -1766,57 +1817,6 @@ namespace EllieBot.Migrations.PostgreSql
|
||||||
b.ToTable("ncpixel", (string)null);
|
b.ToTable("ncpixel", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("EllieBot.Db.Models.EllieExpression", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("integer")
|
|
||||||
.HasColumnName("id");
|
|
||||||
|
|
||||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
|
||||||
|
|
||||||
b.Property<bool>("AllowTarget")
|
|
||||||
.HasColumnType("boolean")
|
|
||||||
.HasColumnName("allowtarget");
|
|
||||||
|
|
||||||
b.Property<bool>("AutoDeleteTrigger")
|
|
||||||
.HasColumnType("boolean")
|
|
||||||
.HasColumnName("autodeletetrigger");
|
|
||||||
|
|
||||||
b.Property<bool>("ContainsAnywhere")
|
|
||||||
.HasColumnType("boolean")
|
|
||||||
.HasColumnName("containsanywhere");
|
|
||||||
|
|
||||||
b.Property<DateTime?>("DateAdded")
|
|
||||||
.HasColumnType("timestamp without time zone")
|
|
||||||
.HasColumnName("dateadded");
|
|
||||||
|
|
||||||
b.Property<bool>("DmResponse")
|
|
||||||
.HasColumnType("boolean")
|
|
||||||
.HasColumnName("dmresponse");
|
|
||||||
|
|
||||||
b.Property<decimal?>("GuildId")
|
|
||||||
.HasColumnType("numeric(20,0)")
|
|
||||||
.HasColumnName("guildid");
|
|
||||||
|
|
||||||
b.Property<string>("Reactions")
|
|
||||||
.HasColumnType("text")
|
|
||||||
.HasColumnName("reactions");
|
|
||||||
|
|
||||||
b.Property<string>("Response")
|
|
||||||
.HasColumnType("text")
|
|
||||||
.HasColumnName("response");
|
|
||||||
|
|
||||||
b.Property<string>("Trigger")
|
|
||||||
.HasColumnType("text")
|
|
||||||
.HasColumnName("trigger");
|
|
||||||
|
|
||||||
b.HasKey("Id")
|
|
||||||
.HasName("pk_expressions");
|
|
||||||
|
|
||||||
b.ToTable("expressions", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("EllieBot.Db.Models.Notify", b =>
|
modelBuilder.Entity("EllieBot.Db.Models.Notify", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
|
@ -3374,6 +3374,40 @@ namespace EllieBot.Migrations.PostgreSql
|
||||||
b.ToTable("xpshopowneditem", (string)null);
|
b.ToTable("xpshopowneditem", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("EllieBot.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("EllieBot.Services.GreetSettings", b =>
|
modelBuilder.Entity("EllieBot.Services.GreetSettings", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
|
|
3198
src/EllieBot/Migrations/Sqlite/20250114055850_fishes.Designer.cs
generated
Normal file
3198
src/EllieBot/Migrations/Sqlite/20250114055850_fishes.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
38
src/EllieBot/Migrations/Sqlite/20250114055850_fishes.cs
Normal file
38
src/EllieBot/Migrations/Sqlite/20250114055850_fishes.cs
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace EllieBot.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class fishes : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "FishCatch",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
UserId = table.Column<ulong>(type: "INTEGER", nullable: false),
|
||||||
|
FishId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
Count = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
MaxStars = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_FishCatch", x => x.Id);
|
||||||
|
table.UniqueConstraint("AK_FishCatch_UserId_FishId", x => new { x.UserId, x.FishId });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "FishCatch");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,9 @@
|
||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
|
using EllieBot.Db;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
using EllieBot.Db;
|
|
||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
|
@ -646,6 +646,44 @@ namespace EllieBot.Migrations
|
||||||
b.ToTable("DiscordUser");
|
b.ToTable("DiscordUser");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("EllieBot.Db.Models.EllieExpression", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("AllowTarget")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("AutoDeleteTrigger")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("ContainsAnywhere")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DateAdded")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("DmResponse")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<ulong?>("GuildId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Reactions")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Response")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Trigger")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Expressions");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("EllieBot.Db.Models.ExcludedItem", b =>
|
modelBuilder.Entity("EllieBot.Db.Models.ExcludedItem", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
|
@ -1318,44 +1356,6 @@ namespace EllieBot.Migrations
|
||||||
b.ToTable("NCPixel");
|
b.ToTable("NCPixel");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("EllieBot.Db.Models.EllieExpression", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<bool>("AllowTarget")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<bool>("AutoDeleteTrigger")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<bool>("ContainsAnywhere")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<DateTime?>("DateAdded")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<bool>("DmResponse")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<ulong?>("GuildId")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<string>("Reactions")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Response")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Trigger")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("Expressions");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("EllieBot.Db.Models.Notify", b =>
|
modelBuilder.Entity("EllieBot.Db.Models.Notify", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
|
@ -2508,6 +2508,31 @@ namespace EllieBot.Migrations
|
||||||
b.ToTable("XpShopOwnedItem");
|
b.ToTable("XpShopOwnedItem");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("EllieBot.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("EllieBot.Services.GreetSettings", b =>
|
modelBuilder.Entity("EllieBot.Services.GreetSettings", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
|
|
55
src/EllieBot/Modules/Games/Fish/CaptchaService.cs
Normal file
55
src/EllieBot/Modules/Games/Fish/CaptchaService.cs
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
using SixLabors.Fonts;
|
||||||
|
using SixLabors.Fonts.Unicode;
|
||||||
|
using SixLabors.ImageSharp;
|
||||||
|
using SixLabors.ImageSharp.Drawing.Processing;
|
||||||
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
|
using SixLabors.ImageSharp.Processing;
|
||||||
|
using Color = SixLabors.ImageSharp.Color;
|
||||||
|
|
||||||
|
namespace EllieBot.Modules.Games;
|
||||||
|
|
||||||
|
public sealed class CaptchaService(FontProvider fonts) : IEService
|
||||||
|
{
|
||||||
|
private readonly EllieRandom _rng = new();
|
||||||
|
|
||||||
|
public Image<Rgba32> GetPasswordImage(string password)
|
||||||
|
{
|
||||||
|
var img = new Image<Rgba32>(50, 24);
|
||||||
|
|
||||||
|
var font = fonts.NotoSans.CreateFont(22);
|
||||||
|
var outlinePen = new SolidPen(Color.Black, 0.5f);
|
||||||
|
var strikeoutRun = new RichTextRun
|
||||||
|
{
|
||||||
|
Start = 0,
|
||||||
|
End = password.GetGraphemeCount(),
|
||||||
|
Font = font,
|
||||||
|
StrikeoutPen = new SolidPen(Color.White, 4),
|
||||||
|
TextDecorations = TextDecorations.Strikeout
|
||||||
|
};
|
||||||
|
|
||||||
|
// draw password on the image
|
||||||
|
img.Mutate(x =>
|
||||||
|
{
|
||||||
|
DrawTextExtensions.DrawText(x,
|
||||||
|
new RichTextOptions(font)
|
||||||
|
{
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
FallbackFontFamilies = fonts.FallBackFonts,
|
||||||
|
Origin = new(25, 12),
|
||||||
|
TextRuns = [strikeoutRun]
|
||||||
|
},
|
||||||
|
password,
|
||||||
|
Brushes.Solid(Color.White),
|
||||||
|
outlinePen);
|
||||||
|
});
|
||||||
|
|
||||||
|
return img;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GeneratePassword()
|
||||||
|
{
|
||||||
|
var num = _rng.Next((int)Math.Pow(31, 2), (int)Math.Pow(32, 3));
|
||||||
|
return new kwum(num).ToString();
|
||||||
|
}
|
||||||
|
}
|
27
src/EllieBot/Modules/Games/Fish/FishCatch.cs
Normal file
27
src/EllieBot/Modules/Games/Fish/FishCatch.cs
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace EllieBot.Modules.Games;
|
||||||
|
|
||||||
|
public sealed class FishCatch
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public int Id { get; set; }
|
||||||
|
public ulong UserId { get; set; }
|
||||||
|
public int FishId { get; set; }
|
||||||
|
public int Count { get; set; }
|
||||||
|
public int MaxStars { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class FishCatchConfiguration : IEntityTypeConfiguration<FishCatch>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<FishCatch> builder)
|
||||||
|
{
|
||||||
|
builder.HasAlternateKey(x => new
|
||||||
|
{
|
||||||
|
x.UserId,
|
||||||
|
x.FishId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
8
src/EllieBot/Modules/Games/Fish/FishChance.cs
Normal file
8
src/EllieBot/Modules/Games/Fish/FishChance.cs
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
namespace EllieBot.Modules.Games;
|
||||||
|
|
||||||
|
public sealed class FishChance
|
||||||
|
{
|
||||||
|
public int Fish { get; set; } = 75;
|
||||||
|
public int Trash { get; set; } = 20;
|
||||||
|
public int Nothing { get; set; } = 0;
|
||||||
|
}
|
292
src/EllieBot/Modules/Games/Fish/FishCommands.cs
Normal file
292
src/EllieBot/Modules/Games/Fish/FishCommands.cs
Normal file
|
@ -0,0 +1,292 @@
|
||||||
|
using System.Text;
|
||||||
|
using Format = Discord.Format;
|
||||||
|
|
||||||
|
namespace EllieBot.Modules.Games;
|
||||||
|
|
||||||
|
public partial class Games
|
||||||
|
{
|
||||||
|
public class FishCommands(
|
||||||
|
FishService fs,
|
||||||
|
FishConfigService fcs,
|
||||||
|
IBotCache cache,
|
||||||
|
CaptchaService service) : EllieModule
|
||||||
|
{
|
||||||
|
private TypedKey<bool> FishingWhitelistKey(ulong userId)
|
||||||
|
=> new($"fishingwhitelist:{userId}");
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
public async Task Fish()
|
||||||
|
{
|
||||||
|
var cRes = await cache.GetAsync(FishingWhitelistKey(ctx.User.Id));
|
||||||
|
if (cRes.TryPickT1(out _, out _))
|
||||||
|
{
|
||||||
|
var password = await GetUserCaptcha(ctx.User.Id);
|
||||||
|
var img = service.GetPasswordImage(password);
|
||||||
|
using var stream = await img.ToStreamAsync();
|
||||||
|
var captcha = await Response()
|
||||||
|
.File(stream, "timely.png")
|
||||||
|
.SendAsync();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var userInput = await GetUserInputAsync(ctx.User.Id, ctx.Channel.Id);
|
||||||
|
if (userInput?.ToLowerInvariant() != password?.ToLowerInvariant())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// whitelist the user for 30 minutes
|
||||||
|
await cache.AddAsync(FishingWhitelistKey(ctx.User.Id), true, TimeSpan.FromMinutes(30));
|
||||||
|
// reset the password
|
||||||
|
await ClearUserCaptcha(ctx.User.Id);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_ = captcha.DeleteAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var fishResult = await fs.FishAsync(ctx.User.Id, ctx.Channel.Id);
|
||||||
|
if (fishResult.TryPickT1(out _, out var fishTask))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentWeather = fs.GetCurrentWeather();
|
||||||
|
var currentTod = fs.GetTime();
|
||||||
|
var spot = fs.GetSpot(ctx.Channel.Id);
|
||||||
|
|
||||||
|
var msg = await Response()
|
||||||
|
.Embed(CreateEmbed()
|
||||||
|
.WithPendingColor()
|
||||||
|
.WithAuthor(ctx.User)
|
||||||
|
.WithDescription(GetText(strs.fish_waiting))
|
||||||
|
.AddField(GetText(strs.fish_spot), GetSpotEmoji(spot) + " " + spot.ToString(), true)
|
||||||
|
.AddField(GetText(strs.fish_weather),
|
||||||
|
GetWeatherEmoji(currentWeather) + " " + currentWeather,
|
||||||
|
true)
|
||||||
|
.AddField(GetText(strs.fish_tod), GetTodEmoji(currentTod) + " " + currentTod, true))
|
||||||
|
.SendAsync();
|
||||||
|
|
||||||
|
var res = await fishTask;
|
||||||
|
if (res is null)
|
||||||
|
{
|
||||||
|
await Response().Error(strs.fish_nothing).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
await Response()
|
||||||
|
.Embed(CreateEmbed()
|
||||||
|
.WithOkColor()
|
||||||
|
.WithAuthor(ctx.User)
|
||||||
|
.WithDescription(GetText(strs.fish_caught(Format.Bold(res.Fish.Name))))
|
||||||
|
.AddField(GetText(strs.fish_quality), GetStarText(res.Stars, res.Fish.Stars), true)
|
||||||
|
.AddField(GetText(strs.desc), res.Fish.Fluff, true)
|
||||||
|
.WithThumbnailUrl(res.Fish.Image))
|
||||||
|
.SendAsync();
|
||||||
|
|
||||||
|
await msg.DeleteAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
public async Task FishSpot()
|
||||||
|
{
|
||||||
|
var ws = fs.GetWeatherForPeriods(7);
|
||||||
|
var spot = fs.GetSpot(ctx.Channel.Id);
|
||||||
|
var time = fs.GetTime();
|
||||||
|
|
||||||
|
await Response()
|
||||||
|
.Embed(CreateEmbed()
|
||||||
|
.WithOkColor()
|
||||||
|
.WithDescription(GetText(strs.fish_weather_duration(fs.GetWeatherPeriodDuration())))
|
||||||
|
.AddField(GetText(strs.fish_spot), GetSpotEmoji(spot) + " " + spot, true)
|
||||||
|
.AddField(GetText(strs.fish_tod), GetTodEmoji(time) + " " + time, true)
|
||||||
|
.AddField(GetText(strs.fish_weather_forecast),
|
||||||
|
ws.Select(x => GetWeatherEmoji(x)).Join(""),
|
||||||
|
true))
|
||||||
|
.SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
public async Task Fishlist(int page = 1)
|
||||||
|
{
|
||||||
|
if (--page < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var fishes = await fs.GetAllFish();
|
||||||
|
|
||||||
|
Log.Information(fishes.Count.ToString());
|
||||||
|
var catches = await fs.GetUserCatches(ctx.User.Id);
|
||||||
|
|
||||||
|
var catchDict = catches.ToDictionary(x => x.FishId, x => x);
|
||||||
|
|
||||||
|
await Response()
|
||||||
|
.Paginated()
|
||||||
|
.Items(fishes)
|
||||||
|
.PageSize(9)
|
||||||
|
.CurrentPage(page)
|
||||||
|
.Page((fs, i) =>
|
||||||
|
{
|
||||||
|
var eb = CreateEmbed()
|
||||||
|
.WithOkColor();
|
||||||
|
|
||||||
|
foreach (var f in fs)
|
||||||
|
{
|
||||||
|
if (catchDict.TryGetValue(f.Id, out var c))
|
||||||
|
{
|
||||||
|
eb.AddField(f.Name,
|
||||||
|
GetFishEmoji(f, c.Count)
|
||||||
|
+ " "
|
||||||
|
+ GetSpotEmoji(f.Spot)
|
||||||
|
+ GetTodEmoji(f.Time)
|
||||||
|
+ GetWeatherEmoji(f.Weather)
|
||||||
|
+ "\n"
|
||||||
|
+ GetStarText(c.MaxStars, f.Stars)
|
||||||
|
+ "\n"
|
||||||
|
+ Format.Italics(f.Fluff),
|
||||||
|
true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
eb.AddField("?", GetFishEmoji(null, 0) + "\n" + GetStarText(0, f.Stars), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return eb;
|
||||||
|
})
|
||||||
|
.SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetFishEmoji(FishData? fish, int count)
|
||||||
|
{
|
||||||
|
if (fish is null)
|
||||||
|
return "";
|
||||||
|
|
||||||
|
return fish.Emoji + " x" + count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetSpotEmoji(FishingSpot? spot)
|
||||||
|
{
|
||||||
|
if (spot is not FishingSpot fs)
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
var conf = fcs.Data;
|
||||||
|
|
||||||
|
return conf.SpotEmojis[(int)fs];
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetTodEmoji(FishingTime? fishTod)
|
||||||
|
{
|
||||||
|
return fishTod switch
|
||||||
|
{
|
||||||
|
FishingTime.Night => "🌑",
|
||||||
|
FishingTime.Dawn => "🌅",
|
||||||
|
FishingTime.Dusk => "🌆",
|
||||||
|
FishingTime.Day => "☀️",
|
||||||
|
_ => ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetWeatherEmoji(FishingWeather? w)
|
||||||
|
=> w switch
|
||||||
|
{
|
||||||
|
FishingWeather.Rain => "🌧️",
|
||||||
|
FishingWeather.Snow => "❄️",
|
||||||
|
FishingWeather.Storm => "⛈️",
|
||||||
|
FishingWeather.Clear => "☀️",
|
||||||
|
_ => ""
|
||||||
|
};
|
||||||
|
|
||||||
|
private string GetStarText(int resStars, int fishStars)
|
||||||
|
{
|
||||||
|
if (resStars == fishStars)
|
||||||
|
{
|
||||||
|
return MultiplyStars(fcs.Data.StarEmojis[^1], fishStars);
|
||||||
|
}
|
||||||
|
|
||||||
|
var c = fcs.Data;
|
||||||
|
var starsp1 = MultiplyStars(c.StarEmojis[resStars], resStars);
|
||||||
|
var starsp2 = MultiplyStars(c.StarEmojis[0], fishStars - resStars);
|
||||||
|
|
||||||
|
return starsp1 + starsp2;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string MultiplyStars(string starEmoji, int count)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
|
||||||
|
for (var i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
sb.Append(starEmoji);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TypedKey<string> CaptchaPasswordKey(ulong userId)
|
||||||
|
=> new($"timely_password:{userId}");
|
||||||
|
|
||||||
|
private async Task<string> GetUserCaptcha(ulong userId)
|
||||||
|
{
|
||||||
|
var pw = await cache.GetOrAddAsync(CaptchaPasswordKey(userId),
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
var password = service.GeneratePassword();
|
||||||
|
return Task.FromResult(password)!;
|
||||||
|
});
|
||||||
|
|
||||||
|
return pw!;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ValueTask<bool> ClearUserCaptcha(ulong userId)
|
||||||
|
=> cache.RemoveAsync(CaptchaPasswordKey(userId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// public sealed class UserFishStats
|
||||||
|
// {
|
||||||
|
// [Key]
|
||||||
|
// public int Id { get; set; }
|
||||||
|
//
|
||||||
|
// public ulong UserId { get; set; }
|
||||||
|
//
|
||||||
|
// public ulong CommonCatches { get; set; }
|
||||||
|
// public ulong RareCatches { get; set; }
|
||||||
|
// public ulong VeryRareCatches { get; set; }
|
||||||
|
// public ulong EpicCatches { get; set; }
|
||||||
|
//
|
||||||
|
// public ulong CommonMaxCatches { get; set; }
|
||||||
|
// public ulong RareMaxCatches { get; set; }
|
||||||
|
// public ulong VeryRareMaxCatches { get; set; }
|
||||||
|
// public ulong EpicMaxCatches { get; set; }
|
||||||
|
//
|
||||||
|
// public int TotalStars { get; set; }
|
||||||
|
// }
|
||||||
|
|
||||||
|
public enum FishingSpot
|
||||||
|
{
|
||||||
|
Ocean,
|
||||||
|
River,
|
||||||
|
Lake,
|
||||||
|
Swamp,
|
||||||
|
Reef
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum FishingTime
|
||||||
|
{
|
||||||
|
Night,
|
||||||
|
Dawn,
|
||||||
|
Day,
|
||||||
|
Dusk
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum FishingWeather
|
||||||
|
{
|
||||||
|
Clear,
|
||||||
|
Rain,
|
||||||
|
Storm,
|
||||||
|
Snow
|
||||||
|
}
|
19
src/EllieBot/Modules/Games/Fish/FishConfig.cs
Normal file
19
src/EllieBot/Modules/Games/Fish/FishConfig.cs
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
using Cloneable;
|
||||||
|
using EllieBot.Common.Yml;
|
||||||
|
|
||||||
|
namespace EllieBot.Modules.Games;
|
||||||
|
|
||||||
|
[Cloneable]
|
||||||
|
public sealed partial class FishConfig : ICloneable<FishConfig>
|
||||||
|
{
|
||||||
|
[Comment("DO NOT CHANGE")]
|
||||||
|
public int Version { get; set; } = 1;
|
||||||
|
|
||||||
|
public string WeatherSeed { get; set; } = string.Empty;
|
||||||
|
public List<string> StarEmojis { get; set; } = new();
|
||||||
|
public List<string> SpotEmojis { get; set; } = new();
|
||||||
|
public FishChance Chance { get; set; } = new FishChance();
|
||||||
|
|
||||||
|
public List<FishData> Fish { get; set; } = new();
|
||||||
|
public List<FishData> Trash { get; set; } = new();
|
||||||
|
}
|
19
src/EllieBot/Modules/Games/Fish/FishConfigService.cs
Normal file
19
src/EllieBot/Modules/Games/Fish/FishConfigService.cs
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
using EllieBot.Common.Configs;
|
||||||
|
|
||||||
|
namespace EllieBot.Modules.Games;
|
||||||
|
|
||||||
|
public sealed class FishConfigService : ConfigServiceBase<FishConfig>
|
||||||
|
{
|
||||||
|
private static string FILE_PATH = "data/fish.yml";
|
||||||
|
private static readonly TypedKey<FishConfig> _changeKey = new("config.fish.updated");
|
||||||
|
|
||||||
|
public override string Name
|
||||||
|
=> "fishing";
|
||||||
|
|
||||||
|
public FishConfigService(
|
||||||
|
IConfigSeria serializer,
|
||||||
|
IPubSub pubSub)
|
||||||
|
: base(FILE_PATH, serializer, pubSub, _changeKey)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
16
src/EllieBot/Modules/Games/Fish/FishData.cs
Normal file
16
src/EllieBot/Modules/Games/Fish/FishData.cs
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
namespace EllieBot.Modules.Games;
|
||||||
|
|
||||||
|
public class FishData
|
||||||
|
{
|
||||||
|
public required int Id { get; set; }
|
||||||
|
public required string Name { get; set; }
|
||||||
|
public FishingWeather? Weather { get; set; }
|
||||||
|
public FishingSpot? Spot { get; set; }
|
||||||
|
public FishingTime? Time { get; set; }
|
||||||
|
public required double Chance { get; set; }
|
||||||
|
public required int Stars { get; set; }
|
||||||
|
public required string Fluff { get; set; }
|
||||||
|
public List<string>? Condition { get; set; }
|
||||||
|
public string? Image { get; init; }
|
||||||
|
public string? Emoji { get; set; }
|
||||||
|
}
|
9
src/EllieBot/Modules/Games/Fish/FishResult.cs
Normal file
9
src/EllieBot/Modules/Games/Fish/FishResult.cs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
namespace EllieBot.Modules.Games;
|
||||||
|
|
||||||
|
public sealed class FishResult
|
||||||
|
{
|
||||||
|
public required FishData Fish { get; init; }
|
||||||
|
public int Stars { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly record struct AlreadyFishing;
|
318
src/EllieBot/Modules/Games/Fish/FishService.cs
Normal file
318
src/EllieBot/Modules/Games/Fish/FishService.cs
Normal file
|
@ -0,0 +1,318 @@
|
||||||
|
using LinqToDB;
|
||||||
|
using LinqToDB.EntityFrameworkCore;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
|
namespace EllieBot.Modules.Games;
|
||||||
|
|
||||||
|
public sealed class FishService(FishConfigService fcs, IBotCache cache, DbService db) : IEService
|
||||||
|
{
|
||||||
|
private Random _rng = new Random();
|
||||||
|
|
||||||
|
private static TypedKey<bool> FishingKey(ulong userId)
|
||||||
|
=> new($"fishing:{userId}");
|
||||||
|
|
||||||
|
public async Task<OneOf.OneOf<Task<FishResult?>, AlreadyFishing>> FishAsync(ulong userId, ulong channelId)
|
||||||
|
{
|
||||||
|
var duration = _rng.Next(1, 9);
|
||||||
|
|
||||||
|
if (!await cache.AddAsync(FishingKey(userId), true, TimeSpan.FromSeconds(duration), overwrite: false))
|
||||||
|
{
|
||||||
|
return new AlreadyFishing();
|
||||||
|
}
|
||||||
|
|
||||||
|
return TryFishAsync(userId, channelId, duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<FishResult?> TryFishAsync(ulong userId, ulong channelId, int duration)
|
||||||
|
{
|
||||||
|
var conf = fcs.Data;
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(duration));
|
||||||
|
|
||||||
|
// first roll whether it's fish, trash or nothing
|
||||||
|
var totalChance = conf.Chance.Fish + conf.Chance.Trash + conf.Chance.Nothing;
|
||||||
|
var typeRoll = _rng.NextDouble() * totalChance;
|
||||||
|
|
||||||
|
if (typeRoll < conf.Chance.Nothing)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var items = typeRoll < conf.Chance.Nothing + conf.Chance.Fish
|
||||||
|
? conf.Fish
|
||||||
|
: conf.Trash;
|
||||||
|
|
||||||
|
return await FishAsyncInternal(userId, channelId, items);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<FishResult?> FishAsyncInternal(ulong userId, ulong channelId, List<FishData> items)
|
||||||
|
{
|
||||||
|
var filteredItems = new List<FishData>();
|
||||||
|
|
||||||
|
var loc = GetSpot(channelId);
|
||||||
|
var time = GetTime();
|
||||||
|
var w = GetWeather(DateTime.UtcNow);
|
||||||
|
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
if (item.Condition is { Count: > 0 })
|
||||||
|
{
|
||||||
|
if (!item.Condition.Any(x => channelId.ToString().EndsWith(x)))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (item.Spot is not null && item.Spot != loc)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (item.Time is not null && item.Time != time)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (item.Weather is not null && item.Weather != w)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
filteredItems.Add(item);
|
||||||
|
Log.Information("Added {FishName} to filtered items", item.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
var maxSum = filteredItems.Sum(x => x.Chance * 100);
|
||||||
|
|
||||||
|
|
||||||
|
var roll = _rng.NextDouble() * maxSum;
|
||||||
|
Log.Information("Roll: {Roll}, MaxSum: {MaxSum}", roll, maxSum);
|
||||||
|
|
||||||
|
FishResult? caught = null;
|
||||||
|
|
||||||
|
var curSum = 0d;
|
||||||
|
foreach (var i in filteredItems)
|
||||||
|
{
|
||||||
|
curSum += i.Chance * 100;
|
||||||
|
|
||||||
|
if (roll < curSum)
|
||||||
|
{
|
||||||
|
caught = new FishResult()
|
||||||
|
{
|
||||||
|
Fish = i,
|
||||||
|
Stars = GetRandomStars(i.Stars),
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (caught is not null)
|
||||||
|
{
|
||||||
|
await using var uow = db.GetDbContext();
|
||||||
|
|
||||||
|
await uow.GetTable<FishCatch>()
|
||||||
|
.InsertOrUpdateAsync(() => new FishCatch()
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
FishId = caught.Fish.Id,
|
||||||
|
MaxStars = caught.Stars,
|
||||||
|
Count = 1
|
||||||
|
},
|
||||||
|
(old) => new()
|
||||||
|
{
|
||||||
|
Count = old.Count + 1,
|
||||||
|
MaxStars = Math.Max((int)old.MaxStars, caught.Stars),
|
||||||
|
},
|
||||||
|
() => new()
|
||||||
|
{
|
||||||
|
FishId = caught.Fish.Id,
|
||||||
|
UserId = userId
|
||||||
|
});
|
||||||
|
|
||||||
|
return caught;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.Error(
|
||||||
|
"Something went wrong in the fish command, no fish with sufficient chance was found, Roll: {Roll}, MaxSum: {MaxSum}",
|
||||||
|
roll,
|
||||||
|
maxSum);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public FishingSpot GetSpot(ulong channelId)
|
||||||
|
{
|
||||||
|
var cid = (channelId >> 22 >> 8) & 1;
|
||||||
|
|
||||||
|
return cid switch
|
||||||
|
{
|
||||||
|
< 1 => FishingSpot.Reef,
|
||||||
|
< 3 => FishingSpot.River,
|
||||||
|
< 5 => FishingSpot.Lake,
|
||||||
|
< 7 => FishingSpot.Swamp,
|
||||||
|
_ => FishingSpot.Ocean,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public FishingTime GetTime()
|
||||||
|
{
|
||||||
|
var hour = DateTime.UtcNow.Hour % 12;
|
||||||
|
|
||||||
|
if (hour < 3)
|
||||||
|
return FishingTime.Night;
|
||||||
|
|
||||||
|
if (hour < 4)
|
||||||
|
return FishingTime.Dawn;
|
||||||
|
|
||||||
|
if (hour < 11)
|
||||||
|
return FishingTime.Day;
|
||||||
|
|
||||||
|
return FishingTime.Dusk;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private const int WEATHER_PERIODS_PER_DAY = 12;
|
||||||
|
|
||||||
|
public IReadOnlyList<FishingWeather> GetWeatherForPeriods(int periods)
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
var result = new FishingWeather[periods];
|
||||||
|
|
||||||
|
for (var i = 0; i < periods; i++)
|
||||||
|
{
|
||||||
|
result[i] = GetWeather(now.AddHours(i * GetWeatherPeriodDuration()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public FishingWeather GetCurrentWeather()
|
||||||
|
=> GetWeather(DateTime.UtcNow);
|
||||||
|
|
||||||
|
public FishingWeather GetWeather(DateTime time)
|
||||||
|
=> GetWeather(time, fcs.Data.WeatherSeed);
|
||||||
|
|
||||||
|
private FishingWeather GetWeather(DateTime time, string seed)
|
||||||
|
{
|
||||||
|
var year = time.Year;
|
||||||
|
var dayOfYear = time.DayOfYear;
|
||||||
|
var hour = time.Hour;
|
||||||
|
|
||||||
|
var num = (year * 100_000) + (dayOfYear * 100) + (hour / GetWeatherPeriodDuration());
|
||||||
|
|
||||||
|
Span<byte> dataArray = stackalloc byte[4];
|
||||||
|
BitConverter.TryWriteBytes(dataArray, num);
|
||||||
|
|
||||||
|
Span<byte> seedArray = stackalloc byte[seed.Length];
|
||||||
|
for (var index = 0; index < seed.Length; index++)
|
||||||
|
{
|
||||||
|
var c = seed[index];
|
||||||
|
seedArray[index] = (byte)c;
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> arr = stackalloc byte[dataArray.Length + seedArray.Length];
|
||||||
|
|
||||||
|
dataArray.CopyTo(arr);
|
||||||
|
seedArray.CopyTo(arr[dataArray.Length..]);
|
||||||
|
|
||||||
|
using var algo = SHA512.Create();
|
||||||
|
|
||||||
|
Span<byte> hash = stackalloc byte[64];
|
||||||
|
algo.TryComputeHash(arr, hash, out _);
|
||||||
|
|
||||||
|
byte reduced = 0;
|
||||||
|
foreach (var u in hash)
|
||||||
|
reduced ^= u;
|
||||||
|
|
||||||
|
var r = reduced % 16;
|
||||||
|
|
||||||
|
// return (FishingWeather)r;
|
||||||
|
return r switch
|
||||||
|
{
|
||||||
|
< 5 => FishingWeather.Clear,
|
||||||
|
< 9 => FishingWeather.Rain,
|
||||||
|
< 13 => FishingWeather.Storm,
|
||||||
|
_ => FishingWeather.Snow
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a random number of stars between 1 and maxStars
|
||||||
|
/// if maxStars == 1, returns 1
|
||||||
|
/// if maxStars == 2, returns 1 (66%) or 2 (33%)
|
||||||
|
/// if maxStars == 3, returns 1 (65%) or 2 (25%) or 3 (10%)
|
||||||
|
/// if maxStars == 5, returns 1 (40%) or 2 (30%) or 3 (15%) or 4 (10%) or 5 (5%)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="maxStars">Max Number of stars to generate</param>
|
||||||
|
/// <returns>Random number of stars</returns>
|
||||||
|
private int GetRandomStars(int maxStars)
|
||||||
|
{
|
||||||
|
if (maxStars == 1)
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
if (maxStars == 2)
|
||||||
|
{
|
||||||
|
// 66% chance of 1 star, 33% chance of 2 stars
|
||||||
|
return _rng.NextDouble() < 0.66 ? 1 : 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxStars == 3)
|
||||||
|
{
|
||||||
|
// 65% chance of 1 star, 25% chance of 2 stars, 10% chance of 3 stars
|
||||||
|
var r = _rng.NextDouble();
|
||||||
|
if (r < 0.65)
|
||||||
|
return 1;
|
||||||
|
if (r < 0.9)
|
||||||
|
return 2;
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxStars == 4)
|
||||||
|
{
|
||||||
|
// this should never happen
|
||||||
|
// 50% chance of 1 star, 25% chance of 2 stars, 18% chance of 3 stars, 7% chance of 4 stars
|
||||||
|
var r = _rng.NextDouble();
|
||||||
|
if (r < 0.5)
|
||||||
|
return 1;
|
||||||
|
if (r < 0.7)
|
||||||
|
return 2;
|
||||||
|
if (r < 0.85)
|
||||||
|
return 3;
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxStars == 5)
|
||||||
|
{
|
||||||
|
// 40% chance of 1 star, 30% chance of 2 stars, 15% chance of 3 stars, 10% chance of 4 stars, 5% chance of 5 stars
|
||||||
|
var r = _rng.NextDouble();
|
||||||
|
if (r < 0.4)
|
||||||
|
return 1;
|
||||||
|
if (r < 0.7)
|
||||||
|
return 2;
|
||||||
|
if (r < 0.9)
|
||||||
|
return 3;
|
||||||
|
if (r < 0.95)
|
||||||
|
return 4;
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetWeatherPeriodDuration()
|
||||||
|
=> 24 / WEATHER_PERIODS_PER_DAY;
|
||||||
|
|
||||||
|
public async Task<List<FishData>> GetAllFish()
|
||||||
|
{
|
||||||
|
await Task.Yield();
|
||||||
|
|
||||||
|
var conf = fcs.Data;
|
||||||
|
return conf.Fish.Concat(conf.Trash).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<FishCatch>> GetUserCatches(ulong userId)
|
||||||
|
{
|
||||||
|
await using var ctx = db.GetDbContext();
|
||||||
|
|
||||||
|
var catches = await ctx.GetTable<FishCatch>()
|
||||||
|
.Where(x => x.UserId == userId)
|
||||||
|
.ToListAsyncLinqToDB();
|
||||||
|
|
||||||
|
return catches;
|
||||||
|
}
|
||||||
|
}
|
|
@ -54,13 +54,9 @@ public sealed class EllieRandom : Random
|
||||||
{
|
{
|
||||||
var bytes = new byte[sizeof(double)];
|
var bytes = new byte[sizeof(double)];
|
||||||
_rng.GetBytes(bytes);
|
_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()
|
public override double NextDouble()
|
||||||
{
|
=> Sample();
|
||||||
var bytes = new byte[sizeof(double)];
|
|
||||||
_rng.GetBytes(bytes);
|
|
||||||
return BitConverter.ToDouble(bytes, 0);
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -1560,3 +1560,16 @@ notifyclear:
|
||||||
winlb:
|
winlb:
|
||||||
- winlb
|
- winlb
|
||||||
- wins
|
- wins
|
||||||
|
fish:
|
||||||
|
- fish
|
||||||
|
- fi
|
||||||
|
fishlist:
|
||||||
|
- fishlist
|
||||||
|
- fili
|
||||||
|
- fishes
|
||||||
|
- fil
|
||||||
|
- fishlist
|
||||||
|
fishspot:
|
||||||
|
- fishspot
|
||||||
|
- fisp
|
||||||
|
- fish?
|
43
src/EllieBot/data/fish.yml
Normal file
43
src/EllieBot/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>"
|
|
@ -4905,3 +4905,29 @@ winlb:
|
||||||
params:
|
params:
|
||||||
- page:
|
- page:
|
||||||
desc: "The optional page to display."
|
desc: "The optional page to display."
|
||||||
|
fish:
|
||||||
|
desc: |-
|
||||||
|
Attempt to catch a fish.
|
||||||
|
Different fish live in different places, at different times and during different times of the day.
|
||||||
|
ex:
|
||||||
|
- ''
|
||||||
|
params:
|
||||||
|
- { }
|
||||||
|
fishlist:
|
||||||
|
desc: |-
|
||||||
|
Look at your fish catalogue.
|
||||||
|
Shows how many of each fish you caught and what was the highest quality.
|
||||||
|
For each caught fish, it also shows its required spot, time of day and weather.
|
||||||
|
ex:
|
||||||
|
- ''
|
||||||
|
params:
|
||||||
|
- { }
|
||||||
|
- page:
|
||||||
|
desc: "The optional page to display."
|
||||||
|
fishspot:
|
||||||
|
desc: |-
|
||||||
|
Shows information about the current fish spot, weather and time.
|
||||||
|
ex:
|
||||||
|
- ''
|
||||||
|
params:
|
||||||
|
- { }
|
|
@ -1159,5 +1159,15 @@
|
||||||
"notify_desc_removerolerew": "Triggers when a user loses a role as a reward for reaching a level (xprew).",
|
"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.",
|
"notify_desc_not_found": "No description found for this notify event. Please report this.",
|
||||||
"winlb": "Biggest Wins Leaderboard",
|
"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"
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue