.fishop and .finv
You can list items in `.fishop`
Buy with `.fibuy`
See your inventory with `.finv`
Equip with `.fiuse`
Items are defined in items: array at the bottom of fish.yml
Items will show up in your .fili and bonuses will show up when you do .fish
The migrations for quests were meant to be sorted in 4c2b42ab7f
but it kind of decided to be very stupid.
This commit is contained in:
parent
e4afa1e385
commit
0a1797700c
26 changed files with 1318 additions and 291 deletions
.gitignore
src/EllieBot
Migrations
PostgreSql
20250324230804_quests.sql20250327001838_fishitems.sql20250328075848_quests.sqlPostgreSqlContextModelSnapshot.cs
Sqlite
Modules/Games
Fish
Db
FishConfig.csFishItem.csFishItemCommands.csFishItemService.csFishService.csFishingCommands.csstrings.jsonQuests
_common
migrate.shstrings
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -371,8 +371,7 @@ site/
|
|||
|
||||
.aider.*
|
||||
PROMPT.md
|
||||
.aider*
|
||||
.windsurfrules
|
||||
.*rules
|
||||
|
||||
## Python pip/env files
|
||||
Pipfile
|
||||
|
|
20
src/EllieBot/Migrations/PostgreSql/20250324230804_quests.sql
Normal file
20
src/EllieBot/Migrations/PostgreSql/20250324230804_quests.sql
Normal file
|
@ -0,0 +1,20 @@
|
|||
START TRANSACTION;
|
||||
CREATE TABLE userquest (
|
||||
id integer GENERATED BY DEFAULT AS IDENTITY,
|
||||
questnumber integer NOT NULL,
|
||||
userid numeric(20,0) NOT NULL,
|
||||
questid integer NOT NULL,
|
||||
progress bigint NOT NULL,
|
||||
iscompleted boolean NOT NULL,
|
||||
dateassigned timestamp without time zone NOT NULL,
|
||||
CONSTRAINT pk_userquest PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE INDEX ix_userquest_userid ON userquest (userid);
|
||||
|
||||
CREATE UNIQUE INDEX ix_userquest_userid_questnumber_dateassigned ON userquest (userid, questnumber, dateassigned);
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" (migrationid, productversion)
|
||||
VALUES ('20250324230804_quests', '9.0.1');
|
||||
|
||||
COMMIT;
|
|
@ -0,0 +1,25 @@
|
|||
START TRANSACTION;
|
||||
ALTER TABLE userfishstats DROP COLUMN bait;
|
||||
|
||||
ALTER TABLE userfishstats DROP COLUMN pole;
|
||||
|
||||
ALTER TABLE userquest ALTER COLUMN progress TYPE integer;
|
||||
|
||||
CREATE TABLE userfishitem (
|
||||
|
||||
id integer GENERATED BY DEFAULT AS IDENTITY,
|
||||
userid numeric(20,0) NOT NULL,
|
||||
itemtype integer NOT NULL,
|
||||
itemid integer NOT NULL,
|
||||
isequipped boolean NOT NULL,
|
||||
usesleft integer,
|
||||
expiresat timestamp without time zone,
|
||||
CONSTRAINT pk_userfishitem PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE INDEX ix_userfishitem_userid ON userfishitem (userid);
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" (migrationid, productversion)
|
||||
VALUES ('20250327001838_fishitems', '9.0.1');
|
||||
|
||||
COMMIT;
|
|
@ -1,6 +0,0 @@
|
|||
START TRANSACTION;
|
||||
INSERT INTO "__EFMigrationsHistory" (migrationid, productversion)
|
||||
VALUES ('20250328075848_quests', '9.0.1');
|
||||
|
||||
COMMIT;
|
||||
|
|
@ -864,57 +864,6 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
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.FeedSub", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
@ -1859,6 +1808,57 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
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 =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
@ -2985,6 +2985,52 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
b.ToTable("unroletimer", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Db.Models.UserQuest", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("DateAssigned")
|
||||
.HasColumnType("timestamp without time zone")
|
||||
.HasColumnName("dateassigned");
|
||||
|
||||
b.Property<bool>("IsCompleted")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("iscompleted");
|
||||
|
||||
b.Property<int>("Progress")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("progress");
|
||||
|
||||
b.Property<int>("QuestId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("questid");
|
||||
|
||||
b.Property<int>("QuestNumber")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("questnumber");
|
||||
|
||||
b.Property<decimal>("UserId")
|
||||
.HasColumnType("numeric(20,0)")
|
||||
.HasColumnName("userid");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_userquest");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_userquest_userid");
|
||||
|
||||
b.HasIndex("UserId", "QuestNumber", "DateAssigned")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_userquest_userid_questnumber_dateassigned");
|
||||
|
||||
b.ToTable("userquest", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Db.Models.UserXpStats", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
@ -3467,6 +3513,48 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
b.ToTable("xpshopowneditem", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Modules.Games.Fish.Db.UserFishItem", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime?>("ExpiresAt")
|
||||
.HasColumnType("timestamp without time zone")
|
||||
.HasColumnName("expiresat");
|
||||
|
||||
b.Property<bool>("IsEquipped")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("isequipped");
|
||||
|
||||
b.Property<int>("ItemId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("itemid");
|
||||
|
||||
b.Property<int>("ItemType")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("itemtype");
|
||||
|
||||
b.Property<decimal>("UserId")
|
||||
.HasColumnType("numeric(20,0)")
|
||||
.HasColumnName("userid");
|
||||
|
||||
b.Property<int?>("UsesLeft")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("usesleft");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_userfishitem");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_userfishitem_userid");
|
||||
|
||||
b.ToTable("userfishitem", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Modules.Games.FishCatch", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
@ -3510,14 +3598,6 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int?>("Bait")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("bait");
|
||||
|
||||
b.Property<int?>("Pole")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("pole");
|
||||
|
||||
b.Property<int>("Skill")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("skill");
|
||||
|
@ -4181,4 +4261,4 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
19
src/EllieBot/Migrations/Sqlite/20250324230801_quests.sql
Normal file
19
src/EllieBot/Migrations/Sqlite/20250324230801_quests.sql
Normal file
|
@ -0,0 +1,19 @@
|
|||
BEGIN TRANSACTION;
|
||||
CREATE TABLE "UserQuest" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_UserQuest" PRIMARY KEY AUTOINCREMENT,
|
||||
"QuestNumber" INTEGER NOT NULL,
|
||||
"UserId" INTEGER NOT NULL,
|
||||
"QuestId" INTEGER NOT NULL,
|
||||
"Progress" INTEGER NOT NULL,
|
||||
"IsCompleted" INTEGER NOT NULL,
|
||||
"DateAssigned" TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX "IX_UserQuest_UserId" ON "UserQuest" ("UserId");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_UserQuest_UserId_QuestNumber_DateAssigned" ON "UserQuest" ("UserId", "QuestNumber", "DateAssigned");
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20250324230801_quests', '9.0.1');
|
||||
|
||||
COMMIT;
|
43
src/EllieBot/Migrations/Sqlite/20250327001835_fishitems.sql
Normal file
43
src/EllieBot/Migrations/Sqlite/20250327001835_fishitems.sql
Normal file
|
@ -0,0 +1,43 @@
|
|||
BEGIN TRANSACTION;
|
||||
CREATE TABLE "UserFishItem" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_UserFishItem" PRIMARY KEY AUTOINCREMENT,
|
||||
"UserId" INTEGER NOT NULL,
|
||||
"ItemType" INTEGER NOT NULL,
|
||||
"ItemId" INTEGER NOT NULL,
|
||||
"IsEquipped" INTEGER NOT NULL,
|
||||
"UsesLeft" INTEGER NULL,
|
||||
"ExpiresAt" TEXT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX "IX_UserFishItem_UserId" ON "UserFishItem" ("UserId");
|
||||
|
||||
CREATE TABLE "ef_temp_UserFishStats" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_UserFishStats" PRIMARY KEY AUTOINCREMENT,
|
||||
"Skill" INTEGER NOT NULL,
|
||||
"UserId" INTEGER NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO "ef_temp_UserFishStats" ("Id", "Skill", "UserId")
|
||||
SELECT "Id", "Skill", "UserId"
|
||||
FROM "UserFishStats";
|
||||
|
||||
COMMIT;
|
||||
|
||||
PRAGMA foreign_keys = 0;
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
DROP TABLE "UserFishStats";
|
||||
|
||||
ALTER TABLE "ef_temp_UserFishStats" RENAME TO "UserFishStats";
|
||||
|
||||
COMMIT;
|
||||
|
||||
PRAGMA foreign_keys = 1;
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
CREATE UNIQUE INDEX "IX_UserFishStats_UserId" ON "UserFishStats" ("UserId");
|
||||
|
||||
COMMIT;
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20250327001835_fishitems', '9.0.1');
|
|
@ -1,6 +0,0 @@
|
|||
BEGIN TRANSACTION;
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20250328075818_quests', '9.0.1');
|
||||
|
||||
COMMIT;
|
||||
|
|
@ -646,44 +646,6 @@ namespace EllieBot.Migrations.Sqlite
|
|||
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.FeedSub", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
@ -1388,6 +1350,44 @@ namespace EllieBot.Migrations.Sqlite
|
|||
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 =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
@ -2224,6 +2224,40 @@ namespace EllieBot.Migrations.Sqlite
|
|||
b.ToTable("UnroleTimer");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Db.Models.UserQuest", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("DateAssigned")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsCompleted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Progress")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("QuestId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("QuestNumber")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<ulong>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.HasIndex("UserId", "QuestNumber", "DateAssigned")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("UserQuest");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Db.Models.UserXpStats", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
@ -2579,6 +2613,37 @@ namespace EllieBot.Migrations.Sqlite
|
|||
b.ToTable("XpShopOwnedItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Modules.Games.Fish.Db.UserFishItem", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("ExpiresAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsEquipped")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ItemId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ItemType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<ulong>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("UsesLeft")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("UserFishItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EllieBot.Modules.Games.FishCatch", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
@ -2610,12 +2675,6 @@ namespace EllieBot.Migrations.Sqlite
|
|||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("Bait")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("Pole")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Skill")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
|
@ -3178,4 +3237,4 @@ namespace EllieBot.Migrations.Sqlite
|
|||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
70
src/EllieBot/Modules/Games/Fish/Db/UserFishItem.cs
Normal file
70
src/EllieBot/Modules/Games/Fish/Db/UserFishItem.cs
Normal file
|
@ -0,0 +1,70 @@
|
|||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using EllieBot.Db;
|
||||
using EllieBot.Modules.Games.Fish;
|
||||
|
||||
namespace EllieBot.Modules.Games.Fish.Db;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a fishing item owned by a user.
|
||||
/// </summary>
|
||||
public class UserFishItem
|
||||
{
|
||||
/// <summary>
|
||||
/// The unique identifier for this user fish item.
|
||||
/// </summary>
|
||||
[Key]
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the user who owns this item.
|
||||
/// </summary>
|
||||
public ulong UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of the fishing item.
|
||||
/// </summary>
|
||||
public FishItemType ItemType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the fishing item.
|
||||
/// </summary>
|
||||
public int ItemId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the item is currently equipped by the user.
|
||||
/// </summary>
|
||||
public bool IsEquipped { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of uses left for this item. Null means unlimited uses.
|
||||
/// </summary>
|
||||
public int? UsesLeft { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The date and time when this item expires. Null means the item doesn't expire.
|
||||
/// </summary>
|
||||
public DateTime? ExpiresAt { get; set; }
|
||||
|
||||
|
||||
public int? ExpiryFromNowInMinutes()
|
||||
{
|
||||
if (ExpiresAt is null)
|
||||
return null;
|
||||
|
||||
return (int)(ExpiresAt.Value - DateTime.UtcNow).TotalMinutes;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Entity configuration for UserFishItem.
|
||||
/// </summary>
|
||||
public class UserFishItemConfiguration : IEntityTypeConfiguration<UserFishItem>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<UserFishItem> builder)
|
||||
{
|
||||
builder.HasIndex(x => new { x.UserId });
|
||||
}
|
||||
}
|
|
@ -9,20 +9,4 @@ public sealed class UserFishStats
|
|||
|
||||
public ulong UserId { get; set; }
|
||||
public int Skill { get; set; }
|
||||
|
||||
public int? Pole { get; set; }
|
||||
public int? Bait { get; set; }
|
||||
}
|
||||
|
||||
// public sealed class FishingPole
|
||||
// {
|
||||
// [Key]
|
||||
// public int Id { get; set; }
|
||||
|
||||
// public string Name { get; set; } = string.Empty;
|
||||
|
||||
// public long Price { get; set; }
|
||||
|
||||
// public string Emoji { get; set; } = string.Empty;
|
||||
|
||||
// }
|
||||
}
|
|
@ -14,43 +14,8 @@ public sealed partial class FishConfig : ICloneable<FishConfig>
|
|||
public List<string> StarEmojis { get; set; } = new();
|
||||
public List<string> SpotEmojis { get; set; } = new();
|
||||
public FishChance Chance { get; set; } = new FishChance();
|
||||
// public List<FishBait> Baits { get; set; } = new();
|
||||
// public List<FishingPole> Poles { get; set; } = new();
|
||||
|
||||
public List<FishData> Fish { get; set; } = new();
|
||||
public List<FishData> Trash { get; set; } = new();
|
||||
}
|
||||
|
||||
// public sealed class FishBait : ICloneable<FishBait>
|
||||
// {
|
||||
// public int Id { get; set; }
|
||||
// public string Name { get; set; } = string.Empty;
|
||||
// public long Price { get; set; }
|
||||
// public string Emoji { get; set; } = string.Empty;
|
||||
// public int StackSize { get; set; } = 100;
|
||||
//
|
||||
// public string? OnlyWeather { get; set; }
|
||||
// public string? OnlySpot { get; set; }
|
||||
// public string? OnlyTime { get; set; }
|
||||
//
|
||||
// public double FishMulti { get; set; } = 1;
|
||||
// public double TrashMulti { get; set; } = 1;
|
||||
// public double NothingMulti { get; set; } = 1;
|
||||
//
|
||||
// public double RareFishMulti { get; set; } = 1;
|
||||
// public double RareTrashMulti { get; set; } = 1;
|
||||
//
|
||||
// public double MaxStarMulti { get; set; } = 1;
|
||||
// }
|
||||
//
|
||||
// public sealed class FishingPole : ICloneable<FishingPole>
|
||||
// {
|
||||
// public int Id { get; set; }
|
||||
// public string Name { get; set; } = string.Empty;
|
||||
// public long Price { get; set; }
|
||||
// public string Emoji { get; set; } = string.Empty;
|
||||
// public string Img { get; set; } = string.Empty;
|
||||
//
|
||||
// public double FishMulti { get; set; } = 1;
|
||||
// public double TrashMulti { get; set; } = 1;
|
||||
// public double NothingMulti { get; set; } = 1;
|
||||
// }
|
||||
public List<FishItem> Items { get; set; } = new();
|
||||
}
|
98
src/EllieBot/Modules/Games/Fish/FishItem.cs
Normal file
98
src/EllieBot/Modules/Games/Fish/FishItem.cs
Normal file
|
@ -0,0 +1,98 @@
|
|||
namespace EllieBot.Modules.Games;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an item used in the fishing game.
|
||||
/// </summary>
|
||||
public class FishItem
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique identifier for the item.
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Type of the fishing item (pole, bait, boat, potion).
|
||||
/// </summary>
|
||||
public FishItemType ItemType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Name of the item.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Item Emoji
|
||||
/// </summary>
|
||||
public string Emoji { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Description of the item.
|
||||
/// </summary>
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Price of the item.
|
||||
/// </summary>
|
||||
public int Price { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of times the item can be used. Null means unlimited uses.
|
||||
/// </summary>
|
||||
public int? Uses { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Duration of the item's effect in minutes. Null means permanent effect.
|
||||
/// </summary>
|
||||
public int? DurationMinutes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Multiplier affecting the fish catch rate.
|
||||
/// </summary>
|
||||
public double? FishMultiplier { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Multiplier affecting the trash catch rate.
|
||||
/// </summary>
|
||||
public double? TrashMultiplier { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Multiplier affecting the maximum star rating of caught fish.
|
||||
/// </summary>
|
||||
public double? MaxStarMultiplier { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Multiplier affecting the chance of catching rare fish.
|
||||
/// </summary>
|
||||
public double? RareMultiplier { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Multiplier affecting the fishing speed.
|
||||
/// </summary>
|
||||
public double? FishingSpeedMultiplier { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines the types of items available in the fishing game.
|
||||
/// </summary>
|
||||
public enum FishItemType
|
||||
{
|
||||
/// <summary>
|
||||
/// Fishing pole used to catch fish.
|
||||
/// </summary>
|
||||
Pole,
|
||||
|
||||
/// <summary>
|
||||
/// Bait used to attract fish.
|
||||
/// </summary>
|
||||
Bait,
|
||||
|
||||
/// <summary>
|
||||
/// Boat used for fishing.
|
||||
/// </summary>
|
||||
Boat,
|
||||
|
||||
/// <summary>
|
||||
/// Potion that provides temporary effects.
|
||||
/// </summary>
|
||||
Potion
|
||||
}
|
240
src/EllieBot/Modules/Games/Fish/FishItemCommands.cs
Normal file
240
src/EllieBot/Modules/Games/Fish/FishItemCommands.cs
Normal file
|
@ -0,0 +1,240 @@
|
|||
using EllieBot.Modules.Games.Fish.Db;
|
||||
|
||||
namespace EllieBot.Modules.Games;
|
||||
|
||||
public partial class Games
|
||||
{
|
||||
public class FishItemCommands(FishItemService fis, ICurrencyProvider cp) : EllieModule
|
||||
{
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task FishShop()
|
||||
{
|
||||
var items = fis.GetItems();
|
||||
|
||||
await Response()
|
||||
.Paginated()
|
||||
.Items(items)
|
||||
.PageSize(9)
|
||||
.CurrentPage(0)
|
||||
.Page((pageItems, i) =>
|
||||
{
|
||||
var eb = CreateEmbed()
|
||||
.WithTitle(GetText(strs.fish_items_title))
|
||||
.WithFooter("`.fibuy <id>` to by an item")
|
||||
.WithOkColor();
|
||||
|
||||
foreach (var item in pageItems)
|
||||
{
|
||||
var description = GetItemDescription(item);
|
||||
eb.AddField($"{item.Id}",
|
||||
$"""
|
||||
{description}
|
||||
|
||||
""",
|
||||
true);
|
||||
}
|
||||
|
||||
return eb;
|
||||
})
|
||||
.AddFooter(false)
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
private string GetItemDescription(FishItem item, UserFishItem? userItem = null)
|
||||
{
|
||||
var multiplierInfo = GetMultiplierInfo(item);
|
||||
|
||||
var priceText = userItem is null
|
||||
? $"【 **{CurrencyHelper.N(item.Price, Culture, cp.GetCurrencySign())}** 】"
|
||||
: "";
|
||||
|
||||
return $"""
|
||||
《 **{item.Name}** 》
|
||||
{GetEmoji(item.ItemType)} `{item.ItemType.ToString().ToLower()}` {priceText}
|
||||
{item.Description}
|
||||
{GetItemNotes(item, userItem)}
|
||||
{multiplierInfo}
|
||||
""";
|
||||
}
|
||||
|
||||
private string GetItemNotes(FishItem item, UserFishItem? userItem)
|
||||
{
|
||||
var stats = new List<string>();
|
||||
|
||||
if (item.Uses.HasValue)
|
||||
stats.Add($"**Uses:** {userItem?.UsesLeft ?? item.Uses}");
|
||||
|
||||
if (item.DurationMinutes.HasValue)
|
||||
stats.Add($"**Duration:** {userItem?.ExpiryFromNowInMinutes() ?? item.DurationMinutes}m");
|
||||
|
||||
var toReturn = stats.Count > 0 ? string.Join(" | ", stats) + "\n" : "\n";
|
||||
|
||||
return "\n" + toReturn;
|
||||
}
|
||||
|
||||
public static string GetEmoji(FishItemType itemType)
|
||||
=> itemType switch
|
||||
{
|
||||
FishItemType.Pole => @"\🎣",
|
||||
FishItemType.Boat => @"\⛵",
|
||||
FishItemType.Bait => @"\🍥",
|
||||
FishItemType.Potion => @"\🍷",
|
||||
_ => ""
|
||||
};
|
||||
|
||||
private string GetMultiplierInfo(FishItem item)
|
||||
{
|
||||
var multipliers = new FishMultipliers()
|
||||
{
|
||||
FishMultiplier = item.FishMultiplier ?? 1,
|
||||
TrashMultiplier = item.TrashMultiplier ?? 1,
|
||||
RareMultiplier = item.RareMultiplier ?? 1,
|
||||
StarMultiplier = item.MaxStarMultiplier ?? 1,
|
||||
FishingSpeedMultiplier = item.FishingSpeedMultiplier ?? 1
|
||||
};
|
||||
|
||||
return GetMultiplierInfo(multipliers);
|
||||
}
|
||||
|
||||
|
||||
public static string GetMultiplierInfo(FishMultipliers item)
|
||||
{
|
||||
var multipliers = new List<string>();
|
||||
if (item.FishMultiplier is not 1.0d)
|
||||
multipliers.Add($"{AsPercent(item.FishMultiplier)} chance to catch fish");
|
||||
|
||||
if (item.TrashMultiplier is not 1.0d)
|
||||
multipliers.Add($"{AsPercent(item.TrashMultiplier)} chance to catch trash");
|
||||
|
||||
if (item.RareMultiplier is not 1.0d)
|
||||
multipliers.Add($"{AsPercent(item.RareMultiplier)} chance to catch rare fish");
|
||||
|
||||
if (item.StarMultiplier is not 1.0d)
|
||||
multipliers.Add($"{AsPercent(item.StarMultiplier)} to max star rating");
|
||||
|
||||
if (item.FishingSpeedMultiplier is not 1.0d)
|
||||
multipliers.Add($"{AsPercent(item.FishingSpeedMultiplier)} fishing speed");
|
||||
|
||||
return multipliers.Count > 0
|
||||
? $"{string.Join("\n", multipliers)}\n"
|
||||
: "";
|
||||
}
|
||||
|
||||
private static string AsPercent(double multiplier)
|
||||
{
|
||||
var percentage = (int)((multiplier - 1.0f) * 100);
|
||||
return percentage >= 0 ? $"**+{percentage}%**" : $"**{percentage}%**";
|
||||
}
|
||||
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task FishBuy(int itemId)
|
||||
{
|
||||
var res = await fis.BuyItemAsync(ctx.User.Id, itemId);
|
||||
|
||||
if (res.TryPickT1(out var err, out var eqItem))
|
||||
{
|
||||
if (err == BuyResult.InsufficientFunds)
|
||||
await Response().Error(strs.not_enough(cp.GetCurrencySign())).SendAsync();
|
||||
else
|
||||
await Response().Error(strs.fish_item_not_found).SendAsync();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var embed = CreateEmbed()
|
||||
.WithDescription(GetText(strs.fish_buy_success))
|
||||
.AddField(eqItem.Name, GetMultiplierInfo(eqItem));
|
||||
|
||||
await Response()
|
||||
.Embed(embed)
|
||||
.Interaction(_inter.Create(ctx.User.Id,
|
||||
new ButtonBuilder("Inventory", Guid.NewGuid().ToString(), ButtonStyle.Secondary),
|
||||
(smc) => FishInv()))
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task FishUse(int index)
|
||||
{
|
||||
var eqItem = await fis.EquipItemAsync(ctx.User.Id, index);
|
||||
|
||||
if (eqItem is null)
|
||||
{
|
||||
await Response().Error(strs.fish_item_not_found).SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
var embed = CreateEmbed()
|
||||
.WithDescription(GetText(strs.fish_use_success))
|
||||
.AddField(eqItem.Name, GetMultiplierInfo(eqItem));
|
||||
|
||||
await Response().Embed(embed).SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task FishUnequip(FishItemType itemType)
|
||||
{
|
||||
var res = await fis.UnequipItemAsync(ctx.User.Id, itemType);
|
||||
|
||||
if (res == UnequipResult.Success)
|
||||
await Response().Confirm(strs.fish_unequip_success).SendAsync();
|
||||
else if (res == UnequipResult.NotFound)
|
||||
await Response().Error(strs.fish_item_not_found).SendAsync();
|
||||
else
|
||||
await Response().Error(strs.fish_cant_uneq_potion).SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task FishInv()
|
||||
{
|
||||
var userItems = await fis.GetUserItemsAsync(ctx.User.Id);
|
||||
|
||||
await Response()
|
||||
.Paginated()
|
||||
.Items(userItems)
|
||||
.PageSize(9)
|
||||
.Page((items, page) =>
|
||||
{
|
||||
page += 1;
|
||||
var eb = CreateEmbed()
|
||||
.WithAuthor(ctx.User)
|
||||
.WithTitle(GetText(strs.fish_inv_title))
|
||||
.WithFooter($"`.fiuse <num>` to use/equip an item")
|
||||
.WithOkColor();
|
||||
|
||||
for (var i = 0; i < items.Count; i++)
|
||||
{
|
||||
var (userItem, item) = items[i];
|
||||
var isEquipped = userItem.IsEquipped;
|
||||
|
||||
if (item is null)
|
||||
{
|
||||
eb.AddField($"{(page * 9) + i + 1} | Item not found", $"ID: {userItem.Id}", true);
|
||||
continue;
|
||||
}
|
||||
|
||||
var description = GetItemDescription(item, userItem);
|
||||
|
||||
if (isEquipped)
|
||||
description = "🫴 **IN USE**\n" + description;
|
||||
|
||||
eb.AddField($"{i + 1} | {item.Name} ",
|
||||
$"""
|
||||
{description}
|
||||
""",
|
||||
true);
|
||||
}
|
||||
|
||||
return eb;
|
||||
})
|
||||
.AddFooter(false)
|
||||
.SendAsync();
|
||||
}
|
||||
}
|
||||
}
|
276
src/EllieBot/Modules/Games/Fish/FishItemService.cs
Normal file
276
src/EllieBot/Modules/Games/Fish/FishItemService.cs
Normal file
|
@ -0,0 +1,276 @@
|
|||
using LinqToDB;
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
using EllieBot.Modules.Games.Fish.Db;
|
||||
|
||||
namespace EllieBot.Modules.Games;
|
||||
|
||||
/// <summary>
|
||||
/// Service for managing fish items that users can buy, equip, and use.
|
||||
/// </summary>
|
||||
public sealed class FishItemService(
|
||||
DbService db,
|
||||
ICurrencyService cs,
|
||||
FishConfigService fcs) : IEService
|
||||
{
|
||||
private IReadOnlyList<FishItem> _items
|
||||
=> fcs.Data.Items;
|
||||
|
||||
/// <summary>
|
||||
/// Gets all available fish items.
|
||||
/// </summary>
|
||||
public IReadOnlyList<FishItem> GetItems()
|
||||
=> _items;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a specific fish item by ID.
|
||||
/// </summary>
|
||||
public FishItem? GetItem(int id)
|
||||
=> _items.FirstOrDefault(i => i.Id == id);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all items of a specific type.
|
||||
/// </summary>
|
||||
public List<FishItem> GetItemsByType(FishItemType type)
|
||||
=> _items.Where(i => i.ItemType == type).ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Gets all items owned by a user.
|
||||
/// </summary>
|
||||
public async Task<List<(UserFishItem UserItem, FishItem? Item)>> GetUserItemsAsync(ulong userId)
|
||||
{
|
||||
await using var ctx = db.GetDbContext();
|
||||
|
||||
var userItems = await ctx.GetTable<UserFishItem>()
|
||||
.Where(x => x.UserId == userId)
|
||||
.ToListAsyncLinqToDB();
|
||||
|
||||
return userItems
|
||||
.Select(ui => (ui, GetItem(ui.ItemId)))
|
||||
.Where(x => x.Item2 != null)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all equipped items for a user.
|
||||
/// </summary>
|
||||
public async Task<List<(UserFishItem UserItem, FishItem Item)>> GetEquippedItemsAsync(ulong userId)
|
||||
{
|
||||
await CheckExpiredItemsAsync(userId);
|
||||
|
||||
await using var ctx = db.GetDbContext();
|
||||
var items = await ctx.GetTable<UserFishItem>()
|
||||
.Where(x => x.UserId == userId && x.IsEquipped)
|
||||
.ToListAsyncLinqToDB();
|
||||
|
||||
var output = new List<(UserFishItem, FishItem)>();
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
var fishItem = GetItem(item.ItemId);
|
||||
if (fishItem is not null)
|
||||
output.Add((item, fishItem));
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Buys an item for a user.
|
||||
/// </summary>
|
||||
public async Task<OneOf.OneOf<FishItem, BuyResult>> BuyItemAsync(ulong userId, int itemId)
|
||||
{
|
||||
var item = GetItem(itemId);
|
||||
if (item is null)
|
||||
return BuyResult.NotFound;
|
||||
|
||||
await using var ctx = db.GetDbContext();
|
||||
|
||||
var removed = await cs.RemoveAsync(userId, item.Price, new("fish_item_purchase", item.Name));
|
||||
if (!removed)
|
||||
return BuyResult.InsufficientFunds;
|
||||
|
||||
// Add item to user's inventory
|
||||
await ctx.GetTable<UserFishItem>()
|
||||
.InsertAsync(() => new UserFishItem
|
||||
{
|
||||
UserId = userId,
|
||||
ItemId = itemId,
|
||||
ItemType = item.ItemType,
|
||||
UsesLeft = item.Uses,
|
||||
IsEquipped = false,
|
||||
});
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Equips an item for a user.
|
||||
/// </summary>
|
||||
public async Task<FishItem?> EquipItemAsync(ulong userId, int index)
|
||||
{
|
||||
await using var ctx = db.GetDbContext();
|
||||
await using var tr = await ctx.Database.BeginTransactionAsync();
|
||||
try
|
||||
{
|
||||
var userItem = await ctx.GetTable<UserFishItem>()
|
||||
.Where(x => x.UserId == userId)
|
||||
.Skip(index - 1)
|
||||
.Take(1)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (userItem is null)
|
||||
return null;
|
||||
|
||||
var fishItem = GetItem(userItem.ItemId);
|
||||
|
||||
if (fishItem is null)
|
||||
return null;
|
||||
|
||||
if (userItem.ItemType == FishItemType.Potion)
|
||||
{
|
||||
var query = ctx.GetTable<UserFishItem>()
|
||||
.Where(x => x.Id == userItem.Id && !x.IsEquipped)
|
||||
.Set(x => x.IsEquipped, true);
|
||||
|
||||
if (fishItem.DurationMinutes is { } dur)
|
||||
query = query
|
||||
.Set(x => x.ExpiresAt, DateTime.UtcNow.AddMinutes(dur));
|
||||
|
||||
await query.UpdateAsync();
|
||||
await tr.CommitAsync();
|
||||
return fishItem;
|
||||
}
|
||||
|
||||
// UnEquip any currently equipped item of the same type
|
||||
// and equip current one
|
||||
await ctx.GetTable<UserFishItem>()
|
||||
.Where(x => x.UserId == userId && x.ItemType == userItem.ItemType)
|
||||
.Set(x => x.IsEquipped, x => x.Id == userItem.Id)
|
||||
.UpdateAsync();
|
||||
|
||||
await tr.CommitAsync();
|
||||
|
||||
return fishItem;
|
||||
}
|
||||
catch
|
||||
{
|
||||
await tr.RollbackAsync();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unequips an item for a user.
|
||||
/// </summary>
|
||||
public async Task<UnequipResult> UnequipItemAsync(ulong userId, FishItemType itemType)
|
||||
{
|
||||
// can't unequip potions
|
||||
if (itemType == FishItemType.Potion)
|
||||
return UnequipResult.Potion;
|
||||
|
||||
await using var ctx = db.GetDbContext();
|
||||
|
||||
var affected = await ctx.GetTable<UserFishItem>()
|
||||
.Where(x => x.UserId == userId && x.ItemType == itemType && x.IsEquipped)
|
||||
.Set(x => x.IsEquipped, false)
|
||||
.UpdateAsync();
|
||||
|
||||
if (affected > 0)
|
||||
return UnequipResult.Success;
|
||||
else
|
||||
return UnequipResult.NotFound;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the multipliers from a user's equipped items.
|
||||
/// </summary>
|
||||
public async Task<FishMultipliers> GetUserMultipliersAsync(ulong userId)
|
||||
{
|
||||
var equippedItems = await GetEquippedItemsAsync(userId);
|
||||
|
||||
var multipliers = new FishMultipliers();
|
||||
|
||||
foreach (var (_, item) in equippedItems)
|
||||
{
|
||||
multipliers.FishMultiplier *= item.FishMultiplier ?? 1;
|
||||
multipliers.TrashMultiplier *= item.TrashMultiplier ?? 1;
|
||||
multipliers.StarMultiplier *= item.MaxStarMultiplier ?? 1;
|
||||
multipliers.RareMultiplier *= item.RareMultiplier ?? 1;
|
||||
multipliers.FishingSpeedMultiplier *= item.FishingSpeedMultiplier ?? 1;
|
||||
}
|
||||
|
||||
return multipliers;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uses a bait item (reduces uses left) when fishing.
|
||||
/// </summary>
|
||||
public async Task<bool> UseBaitAsync(ulong userId)
|
||||
{
|
||||
await using var ctx = db.GetDbContext();
|
||||
|
||||
var updated = await ctx.GetTable<UserFishItem>()
|
||||
.Where(x =>
|
||||
x.UserId == userId &&
|
||||
x.ItemType == FishItemType.Bait &&
|
||||
x.IsEquipped)
|
||||
.Set(x => x.UsesLeft, x => x.UsesLeft - 1)
|
||||
.UpdateWithOutputAsync((o, n) => n);
|
||||
|
||||
if (updated.Length == 0)
|
||||
return false;
|
||||
|
||||
if (updated[0].UsesLeft <= 0)
|
||||
{
|
||||
await ctx.GetTable<UserFishItem>()
|
||||
.DeleteAsync(x => x.Id == updated[0].Id);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks and removes expired items.
|
||||
/// </summary>
|
||||
public async Task CheckExpiredItemsAsync(ulong userId)
|
||||
{
|
||||
await using var ctx = db.GetDbContext();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
await ctx.GetTable<UserFishItem>()
|
||||
.Where(x => x.UserId == userId && x.ExpiresAt.HasValue && x.ExpiresAt < now)
|
||||
.DeleteAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the result of a buy operation.
|
||||
/// </summary>
|
||||
public enum BuyResult
|
||||
{
|
||||
NotFound,
|
||||
InsufficientFunds
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the result of an equip operation.
|
||||
/// </summary>
|
||||
public enum UnequipResult
|
||||
{
|
||||
Success,
|
||||
NotFound,
|
||||
Potion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contains multipliers applied to fishing based on equipped items.
|
||||
/// </summary>
|
||||
public class FishMultipliers
|
||||
{
|
||||
public double FishMultiplier { get; set; } = 1.0;
|
||||
public double TrashMultiplier { get; set; } = 1.0;
|
||||
public double StarMultiplier { get; set; } = 1.0;
|
||||
public double RareMultiplier { get; set; } = 1.0;
|
||||
public double FishingSpeedMultiplier { get; set; } = 1.0;
|
||||
}
|
|
@ -13,7 +13,8 @@ public sealed class FishService(
|
|||
IBotCache cache,
|
||||
DbService db,
|
||||
INotifySubscriber notify,
|
||||
QuestService quests
|
||||
QuestService quests,
|
||||
FishItemService itemService
|
||||
)
|
||||
: IEService
|
||||
{
|
||||
|
@ -24,19 +25,24 @@ public sealed class FishService(
|
|||
private static TypedKey<bool> FishingKey(ulong userId)
|
||||
=> new($"fishing:{userId}");
|
||||
|
||||
public async Task<OneOf.OneOf<Task<FishResult?>, AlreadyFishing>> FishAsync(ulong userId, ulong channelId)
|
||||
public async Task<OneOf.OneOf<Task<FishResult?>, AlreadyFishing>> FishAsync(ulong userId, ulong channelId,
|
||||
FishMultipliers multipliers)
|
||||
{
|
||||
var duration = _rng.Next(3, 6);
|
||||
var duration = _rng.Next(3, 6) / multipliers.FishingSpeedMultiplier;
|
||||
|
||||
if (!await cache.AddAsync(FishingKey(userId), true, TimeSpan.FromSeconds(duration), overwrite: false))
|
||||
{
|
||||
return new AlreadyFishing();
|
||||
}
|
||||
|
||||
return TryFishAsync(userId, channelId, duration);
|
||||
return TryFishAsync(userId, channelId, duration, multipliers);
|
||||
}
|
||||
|
||||
private async Task<FishResult?> TryFishAsync(ulong userId, ulong channelId, int duration)
|
||||
private async Task<FishResult?> TryFishAsync(
|
||||
ulong userId,
|
||||
ulong channelId,
|
||||
double duration,
|
||||
FishMultipliers multipliers)
|
||||
{
|
||||
var conf = fcs.Data;
|
||||
await Task.Delay(TimeSpan.FromSeconds(duration));
|
||||
|
@ -46,8 +52,8 @@ public sealed class FishService(
|
|||
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;
|
||||
var fishChance = conf.Chance.Fish * fishChanceMultiplier * multipliers.FishMultiplier;
|
||||
var trashChance = conf.Chance.Trash * trashChanceMultiplier * multipliers.TrashMultiplier;
|
||||
|
||||
// first roll whether it's fish, trash or nothing
|
||||
var totalChance = fishChance + trashChance + conf.Chance.Nothing;
|
||||
|
@ -59,13 +65,21 @@ public sealed class FishService(
|
|||
return null;
|
||||
}
|
||||
|
||||
var items = typeRoll < nothingChance + fishChance
|
||||
var isFish = typeRoll < nothingChance + fishChance;
|
||||
|
||||
var items = isFish
|
||||
? conf.Fish
|
||||
: conf.Trash;
|
||||
|
||||
var result = await FishAsyncInternal(userId, channelId, items, multipliers);
|
||||
|
||||
// use bait
|
||||
if (result is not null)
|
||||
{
|
||||
await itemService.UseBaitAsync(userId);
|
||||
}
|
||||
|
||||
var result = await FishAsyncInternal(userId, channelId, items);
|
||||
|
||||
// skill
|
||||
if (result is not null)
|
||||
{
|
||||
var isSkillUp = await TrySkillUpAsync(userId, playerSkill);
|
||||
|
@ -91,16 +105,16 @@ public sealed class FishService(
|
|||
GetStarText(result.Stars, result.Fish.Stars)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
await quests.ReportActionAsync(userId,
|
||||
QuestEventType.FishCaught,
|
||||
new()
|
||||
{
|
||||
{ "fish", result.Fish.Name },
|
||||
{ "type", typeRoll < nothingChance + fishChance ? "fish" : "trash" },
|
||||
{ "stars", result.Stars.ToString() }
|
||||
});
|
||||
await quests.ReportActionAsync(userId,
|
||||
QuestEventType.FishCaught,
|
||||
new()
|
||||
{
|
||||
{ "fish", result.Fish.Name },
|
||||
{ "type", typeRoll < nothingChance + fishChance ? "fish" : "trash" },
|
||||
{ "stars", result.Stars.ToString() }
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
@ -118,20 +132,20 @@ public sealed class FishService(
|
|||
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
|
||||
});
|
||||
{
|
||||
UserId = userId,
|
||||
Skill = 1,
|
||||
},
|
||||
(old) => new()
|
||||
{
|
||||
UserId = userId,
|
||||
Skill = old.Skill > maxSkill ? maxSkill : old.Skill + 1
|
||||
},
|
||||
() => new()
|
||||
{
|
||||
UserId = userId,
|
||||
Skill = playerSkill
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -162,7 +176,11 @@ public sealed class FishService(
|
|||
return (skill, (int)MAX_SKILL);
|
||||
}
|
||||
|
||||
private async Task<FishResult?> FishAsyncInternal(ulong userId, ulong channelId, List<FishData> items)
|
||||
private async Task<FishResult?> FishAsyncInternal(
|
||||
ulong userId,
|
||||
ulong channelId,
|
||||
List<FishData> items,
|
||||
FishMultipliers multipliers)
|
||||
{
|
||||
var filteredItems = new List<FishData>();
|
||||
|
||||
|
@ -192,7 +210,20 @@ public sealed class FishService(
|
|||
filteredItems.Add(item);
|
||||
}
|
||||
|
||||
var maxSum = filteredItems.Sum(x => x.Chance * 100);
|
||||
|
||||
var maxSum = filteredItems
|
||||
.Select(x => (x.Id, x.Chance, x.Stars))
|
||||
.Select(x =>
|
||||
{
|
||||
if (x.Chance <= 15)
|
||||
return x with
|
||||
{
|
||||
Chance = x.Chance *= multipliers.RareMultiplier
|
||||
};
|
||||
|
||||
return x;
|
||||
})
|
||||
.Sum(x => { return x.Chance * 100; });
|
||||
|
||||
|
||||
var roll = _rng.NextDouble() * maxSum;
|
||||
|
@ -209,7 +240,7 @@ public sealed class FishService(
|
|||
caught = new FishResult()
|
||||
{
|
||||
Fish = i,
|
||||
Stars = GetRandomStars(i.Stars),
|
||||
Stars = GetRandomStars(i.Stars, multipliers),
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
@ -221,22 +252,22 @@ public sealed class FishService(
|
|||
|
||||
await uow.GetTable<FishCatch>()
|
||||
.InsertOrUpdateAsync(() => new FishCatch()
|
||||
{
|
||||
UserId = userId,
|
||||
FishId = caught.Fish.Id,
|
||||
MaxStars = caught.Stars,
|
||||
Count = 1
|
||||
},
|
||||
(old) => new FishCatch()
|
||||
{
|
||||
Count = old.Count + 1,
|
||||
MaxStars = Math.Max(old.MaxStars, caught.Stars),
|
||||
},
|
||||
() => new()
|
||||
{
|
||||
FishId = caught.Fish.Id,
|
||||
UserId = userId
|
||||
});
|
||||
{
|
||||
UserId = userId,
|
||||
FishId = caught.Fish.Id,
|
||||
MaxStars = caught.Stars,
|
||||
Count = 1
|
||||
},
|
||||
(old) => new FishCatch()
|
||||
{
|
||||
Count = old.Count + 1,
|
||||
MaxStars = Math.Max(old.MaxStars, caught.Stars),
|
||||
},
|
||||
() => new()
|
||||
{
|
||||
FishId = caught.Fish.Id,
|
||||
UserId = userId
|
||||
});
|
||||
|
||||
return caught;
|
||||
}
|
||||
|
@ -353,25 +384,30 @@ public sealed class FishService(
|
|||
/// 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>
|
||||
/// <param name="multipliers"></param>
|
||||
/// <returns>Random number of stars</returns>
|
||||
private int GetRandomStars(int maxStars)
|
||||
private int GetRandomStars(int maxStars, FishMultipliers multipliers)
|
||||
{
|
||||
if (maxStars == 1)
|
||||
return 1;
|
||||
|
||||
var maxStarMulti = multipliers.StarMultiplier;
|
||||
double baseChance;
|
||||
if (maxStars == 2)
|
||||
{
|
||||
// 15% chance of 1 star, 85% chance of 2 stars
|
||||
return _rng.NextDouble() < 0.85 ? 1 : 2;
|
||||
baseChance = Math.Clamp(0.15 * multipliers.StarMultiplier, 0, 1);
|
||||
return _rng.NextDouble() < (1 - baseChance) ? 1 : 2;
|
||||
}
|
||||
|
||||
if (maxStars == 3)
|
||||
{
|
||||
// 65% chance of 1 star, 30% chance of 2 stars, 5% chance of 3 stars
|
||||
baseChance = 0.05 * multipliers.StarMultiplier;
|
||||
var r = _rng.NextDouble();
|
||||
if (r < 0.65)
|
||||
if (r < (1 - baseChance - 0.3))
|
||||
return 1;
|
||||
if (r < 0.95)
|
||||
if (r < (1 - baseChance))
|
||||
return 2;
|
||||
return 3;
|
||||
}
|
||||
|
@ -381,26 +417,28 @@ public sealed class FishService(
|
|||
// 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)
|
||||
baseChance = 0.02 * multipliers.StarMultiplier;
|
||||
if (r < (1 - baseChance - 0.45))
|
||||
return 1;
|
||||
if (r < 0.80)
|
||||
if (r < (1 - baseChance - 0.15))
|
||||
return 2;
|
||||
if (r < 0.98)
|
||||
if (r < (1 - baseChance))
|
||||
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
|
||||
// 40% chance of 1 star, 30% chance of 2 stars, 15% chance of 3 stars, 10% chance of 4 stars, 2% chance of 5 stars
|
||||
var r = _rng.NextDouble();
|
||||
if (r < 0.4)
|
||||
baseChance = 0.02 * multipliers.StarMultiplier;
|
||||
if (r < (1 - baseChance - 0.6))
|
||||
return 1;
|
||||
if (r < 0.7)
|
||||
if (r < (1 - baseChance - 0.3))
|
||||
return 2;
|
||||
if (r < 0.9)
|
||||
if (r < (1 - baseChance - 0.1))
|
||||
return 3;
|
||||
if (r < 0.98)
|
||||
if (r < (1 - baseChance))
|
||||
return 4;
|
||||
return 5;
|
||||
}
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text;
|
||||
using EllieBot.Modules.Games.Fish;
|
||||
using EllieBot.Modules.Games.Fish;
|
||||
using Format = Discord.Format;
|
||||
|
||||
namespace EllieBot.Modules.Games;
|
||||
|
||||
public partial class Games
|
||||
{
|
||||
public class FishCommands(
|
||||
public class FishingCommands(
|
||||
FishService fs,
|
||||
FishItemService fis,
|
||||
FishConfigService fcs,
|
||||
IBotCache cache,
|
||||
CaptchaService captchaService) : EllieModule
|
||||
|
@ -34,7 +33,10 @@ public partial class Games
|
|||
using var stream = await img.ToStreamAsync();
|
||||
|
||||
var toSend = Response()
|
||||
.File(stream, "timely.png");
|
||||
.File(stream, "timely.png")
|
||||
.Embed(CreateEmbed()
|
||||
.WithFooter("captcha: type the text from the image")
|
||||
.WithImageUrl("attachment://timely.png"));
|
||||
|
||||
#if GLOBAL_ELLIE
|
||||
if (_rng.Next(0, 8) == 0)
|
||||
|
@ -64,7 +66,8 @@ public partial class Games
|
|||
}
|
||||
|
||||
|
||||
var fishResult = await fs.FishAsync(ctx.User.Id, ctx.Channel.Id);
|
||||
var multis = await fis.GetUserMultipliersAsync(ctx.User.Id);
|
||||
var fishResult = await fs.FishAsync(ctx.User.Id, ctx.Channel.Id, multis);
|
||||
if (fishResult.TryPickT1(out _, out var fishTask))
|
||||
{
|
||||
return;
|
||||
|
@ -78,7 +81,10 @@ public partial class Games
|
|||
.Embed(CreateEmbed()
|
||||
.WithPendingColor()
|
||||
.WithAuthor(ctx.User)
|
||||
.WithDescription(GetText(strs.fish_waiting))
|
||||
.WithDescription($"""
|
||||
{GetText(strs.fish_waiting)}
|
||||
{FishItemCommands.GetMultiplierInfo(multis)}
|
||||
""")
|
||||
.AddField(GetText(strs.fish_spot), GetSpotEmoji(spot) + " " + spot.ToString(), true)
|
||||
.AddField(GetText(strs.fish_weather),
|
||||
GetWeatherEmoji(currentWeather) + " " + currentWeather,
|
||||
|
@ -133,7 +139,7 @@ public partial class Games
|
|||
}
|
||||
|
||||
[Cmd]
|
||||
public async Task Fishlist(int page = 1)
|
||||
public async Task FishList(int page = 1)
|
||||
{
|
||||
if (--page < 0)
|
||||
return;
|
||||
|
@ -145,20 +151,33 @@ public partial class Games
|
|||
|
||||
var catchDict = catches.ToDictionary(x => x.FishId, x => x);
|
||||
|
||||
var items = await fis.GetEquippedItemsAsync(ctx.User.Id);
|
||||
var desc = $"""
|
||||
🧠 {skill} / {maxSkill}
|
||||
""";
|
||||
|
||||
foreach (var itemType in Enum.GetValues<FishItemType>())
|
||||
{
|
||||
var i = items.Where(x => x.Item.ItemType == itemType).ToArray();
|
||||
|
||||
desc += " · " + FishItemCommands.GetEmoji(itemType) + " " +
|
||||
(i.Any() ? string.Join(", ", i.Select(x => x.Item.Name)) : "None");
|
||||
}
|
||||
|
||||
await Response()
|
||||
.Paginated()
|
||||
.Items(fishes)
|
||||
.PageSize(9)
|
||||
.CurrentPage(page)
|
||||
.Page((fishes, i) =>
|
||||
.Page((pageFish, i) =>
|
||||
{
|
||||
var eb = CreateEmbed()
|
||||
.WithDescription($"🧠 **Skill:** {skill} / {maxSkill}")
|
||||
.WithDescription(desc)
|
||||
.WithAuthor(ctx.User)
|
||||
.WithTitle(GetText(strs.fish_list_title))
|
||||
.WithOkColor();
|
||||
|
||||
foreach (var f in fishes)
|
||||
foreach (var f in pageFish)
|
||||
{
|
||||
if (catchDict.TryGetValue(f.Id, out var c))
|
||||
{
|
||||
|
@ -224,9 +243,6 @@ public partial class Games
|
|||
FishingWeather.Clear => "☀️",
|
||||
_ => ""
|
||||
};
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
21
src/EllieBot/Modules/Games/Fish/strings.json
Normal file
21
src/EllieBot/Modules/Games/Fish/strings.json
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"fish_items_title": "Available Fishing Items",
|
||||
"fish_buy_success": "Item purchased successfully!",
|
||||
"fish_buy_not_found": "Item not found.",
|
||||
"fish_buy_already_owned": "You already own this item.",
|
||||
"fish_buy_insufficient_funds": "You don't have enough currency to buy this item.",
|
||||
"fish_buy_error": "An error occurred while trying to buy the item.",
|
||||
"fish_use_success": "Item equipped successfully!",
|
||||
"fish_use_not_found": "Item not found.",
|
||||
"fish_use_not_owned": "You don't own this item.",
|
||||
"fish_use_expired": "This item has expired.",
|
||||
"fish_use_no_uses": "This item has no uses left.",
|
||||
"fish_use_error": "An error occurred while trying to use the item.",
|
||||
"fish_unequip_success": "Item unequipped successfully!",
|
||||
"fish_unequip_error": "Could not unequip item.",
|
||||
"fish_inv_title": "{0}'s Fishing Inventory",
|
||||
"fish_gift_self": "You can't gift items to yourself.",
|
||||
"fish_gift_not_owned": "You don't own this item.",
|
||||
"fish_gift_equipped": "You can't gift equipped items. Unequip it first.",
|
||||
"fish_gift_success": "Item successfully gifted to {0}!"
|
||||
}
|
|
@ -13,8 +13,6 @@ public sealed class QuestService(
|
|||
DiscordSocketClient client
|
||||
) : IEService, IExecPreCommand
|
||||
{
|
||||
private readonly EllieRandom rng = new();
|
||||
|
||||
private readonly IQuest[] _availableQuests =
|
||||
[
|
||||
new HangmanWinQuest(),
|
||||
|
@ -48,10 +46,10 @@ public sealed class QuestService(
|
|||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
Log.Information("Action reported by {UserId}: {EventType} {Metadata}",
|
||||
userId,
|
||||
eventType,
|
||||
metadata.ToJson());
|
||||
// Log.Information("Action reported by {UserId}: {EventType} {Metadata}",
|
||||
// userId,
|
||||
// eventType,
|
||||
// metadata.ToJson());
|
||||
metadata ??= new();
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
|
@ -140,11 +138,11 @@ public sealed class QuestService(
|
|||
{
|
||||
var today = date.Date;
|
||||
var timeUntilTomorrow = today.AddDays(1) - DateTime.UtcNow;
|
||||
if (!await botCache.AddAsync(UserHasQuestsKey(userId), true, expiry: timeUntilTomorrow))
|
||||
if (!await botCache.AddAsync(UserHasQuestsKey(userId), true, expiry: timeUntilTomorrow, overwrite: false))
|
||||
return;
|
||||
|
||||
await using var uow = db.GetDbContext();
|
||||
var newQuests = GenerateDailyQuestsAsync(userId);
|
||||
var newQuests = GenerateDailyQuestsAsync();
|
||||
for (var i = 0; i < MAX_QUESTS_PER_DAY; i++)
|
||||
{
|
||||
await uow.GetTable<UserQuest>()
|
||||
|
@ -170,7 +168,7 @@ public sealed class QuestService(
|
|||
}
|
||||
}
|
||||
|
||||
private IReadOnlyList<IQuest> GenerateDailyQuestsAsync(ulong userId)
|
||||
private IReadOnlyList<IQuest> GenerateDailyQuestsAsync()
|
||||
{
|
||||
return _availableQuests
|
||||
.ToList()
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using EllieBot.Modules.Games.Quests;
|
||||
|
||||
namespace EllieBot.Db.Models;
|
||||
|
@ -18,4 +20,19 @@ public class UserQuest
|
|||
public bool IsCompleted { get; set; }
|
||||
|
||||
public DateTime DateAssigned { get; set; }
|
||||
}
|
||||
|
||||
public sealed class UserQuestEntityConfiguration : IEntityTypeConfiguration<UserQuest>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<UserQuest> builder)
|
||||
{
|
||||
builder.HasIndex(x => x.UserId);
|
||||
|
||||
builder.HasIndex(x => new
|
||||
{
|
||||
x.UserId,
|
||||
x.QuestNumber,
|
||||
x.DateAssigned
|
||||
}).IsUnique();
|
||||
}
|
||||
}
|
|
@ -46,7 +46,7 @@ public partial class ResponseBuilder
|
|||
GetInteractions()
|
||||
{
|
||||
var leftButton = new ButtonBuilder()
|
||||
.WithStyle(ButtonStyle.Primary)
|
||||
.WithStyle(ButtonStyle.Secondary)
|
||||
.WithCustomId(BUTTON_LEFT)
|
||||
.WithEmote(InteractionHelpers.ArrowLeft)
|
||||
.WithDisabled(lastPage == 0 || currentPage <= 0);
|
||||
|
@ -80,7 +80,7 @@ public partial class ResponseBuilder
|
|||
}
|
||||
|
||||
var rightButton = new ButtonBuilder()
|
||||
.WithStyle(ButtonStyle.Primary)
|
||||
.WithStyle(ButtonStyle.Secondary)
|
||||
.WithCustomId(BUTTON_RIGHT)
|
||||
.WithEmote(InteractionHelpers.ArrowRight)
|
||||
.WithDisabled(lastPage == 0 || currentPage >= lastPage);
|
||||
|
|
|
@ -172,6 +172,8 @@ public sealed class GamblingTxTracker(
|
|||
if (txData is null)
|
||||
return;
|
||||
|
||||
await Task.Yield();
|
||||
|
||||
if (_gamblingTypes.Contains(txData.Type))
|
||||
{
|
||||
globalStats.AddOrUpdate(txData.Type,
|
||||
|
|
|
@ -1,26 +1,25 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Check if migration name is provided
|
||||
if [ -z "$1" ]; then
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "Error: Migration name must be specified."
|
||||
echo "Usage: $0 <MigrationName>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
MIGRATION_NAME=$1
|
||||
MIGRATION_NAME="$1"
|
||||
|
||||
# Step 1: Create initial migration
|
||||
echo "Creating new migration..."
|
||||
|
||||
# Step 1: Create initial migrations
|
||||
dotnet build
|
||||
|
||||
# Getting previous migration names in order to generate SQL scripts
|
||||
dotnet ef migrations add "${MIGRATION_NAME}" --context SqliteContext --output-dir "Migrations/Sqlite" --no-build
|
||||
dotnet ef migrations add "${MIGRATION_NAME}" --context PostgresqlContext --output-dir "Migrations/PostgreSql" --no-build
|
||||
dotnet ef migrations add "$MIGRATION_NAME" --context SqliteContext --output-dir "Migrations/Sqlite" --no-build
|
||||
dotnet ef migrations add "$MIGRATION_NAME" --context PostgresqlContext --output-dir "Migrations/PostgreSql" --no-build
|
||||
|
||||
dotnet build
|
||||
|
||||
# Check for migration creation success
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error: Failed to create migrations"
|
||||
exit 1
|
||||
|
@ -29,34 +28,27 @@ fi
|
|||
# Step 2: Generate SQL scripts
|
||||
echo "Generating diff SQL scripts..."
|
||||
|
||||
NEW_MIGRATION_ID_SQLITE=$(dotnet ef migrations list --context SqliteContext --no-build --no-connect | tail -2 | head -1 | cut -d' ' -f1)
|
||||
NEW_MIGRATION_ID_POSTGRESQL=$(dotnet ef migrations list --context PostgresqlContext --no-build --no-connect | tail -2 | head -1 | cut -d' ' -f1)
|
||||
|
||||
dotnet ef migrations script init $MIGRATION_NAME --context SqliteContext -o "Migrations/Sqlite/${NEW_MIGRATION_ID_SQLITE}.sql" --no-build
|
||||
dotnet ef migrations script init $MIGRATION_NAME --context PostgresqlContext -o "Migrations/Postgresql/${NEW_MIGRATION_ID_POSTGRESQL}.sql" --no-build
|
||||
NEW_MIGRATION_ID_SQLITE=$(dotnet ef migrations list --context SqliteContext --no-build --no-connect | tail -n 2 | head -n 1 | awk '{print $1}')
|
||||
NEW_MIGRATION_ID_POSTGRESQL=$(dotnet ef migrations list --context PostgresqlContext --no-build --no-connect | tail -n 2 | head -n 1 | awk '{print $1}')
|
||||
|
||||
dotnet ef migrations script init "$MIGRATION_NAME" --context SqliteContext -o "Migrations/Sqlite/${NEW_MIGRATION_ID_SQLITE}.sql" --no-build
|
||||
dotnet ef migrations script init "$MIGRATION_NAME" --context PostgresqlContext -o "Migrations/PostgreSql/${NEW_MIGRATION_ID_POSTGRESQL}.sql" --no-build
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error: Failed to generate SQL script"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Step 3: Cleanup migration files
|
||||
echo "Cleaning up all migration files..."
|
||||
|
||||
# Step 3: Clean up migration files by removing everything
|
||||
for file in "Migrations/Sqlite/"*.cs; do
|
||||
echo "Deleting: $(basename "$file")"
|
||||
rm -- "$file"
|
||||
done
|
||||
|
||||
for file in "Migrations/Postgresql/"*.cs; do
|
||||
echo "Deleting: $(basename "$file")"
|
||||
rm -- "$file"
|
||||
done
|
||||
|
||||
# Step 4: Adding new initial migration
|
||||
echo "Creating new initial migration..."
|
||||
find "Migrations/Sqlite" -name "*.cs" -type f -print -delete
|
||||
find "Migrations/PostgreSql" -name "*.cs" -type f -print -delete
|
||||
|
||||
dotnet build
|
||||
|
||||
# Step 4: Create new initial migrations
|
||||
echo "Creating new initial migration..."
|
||||
|
||||
dotnet ef migrations add init --context SqliteContext --output-dir "Migrations/Sqlite" --no-build
|
||||
dotnet ef migrations add init --context PostgresqlContext --output-dir "Migrations/PostgreSql" --no-build
|
|
@ -1660,4 +1660,26 @@ massping:
|
|||
questlog:
|
||||
- questlog
|
||||
- qlog
|
||||
- myquests
|
||||
- myquests
|
||||
fishshop:
|
||||
- fishshop
|
||||
- fishop
|
||||
fishbuy:
|
||||
- fishbuy
|
||||
- fibuy
|
||||
fishuse:
|
||||
- fishuse
|
||||
- fiuse
|
||||
- fiequip
|
||||
- fieq
|
||||
- fiquip
|
||||
fishunequip:
|
||||
- fishunequip
|
||||
- fiuneq
|
||||
- fiunequip
|
||||
- fiunuse
|
||||
- fishunuse
|
||||
fishinv:
|
||||
- fishinv
|
||||
- finv
|
||||
- fiinv
|
|
@ -5214,4 +5214,48 @@ questlog:
|
|||
ex:
|
||||
- ''
|
||||
params:
|
||||
- { }
|
||||
- { }
|
||||
fishshop:
|
||||
desc: |-
|
||||
Opens the fish shop.
|
||||
Lists all fish items available for sale
|
||||
ex:
|
||||
- ''
|
||||
params:
|
||||
- { }
|
||||
fishinv:
|
||||
desc: |-
|
||||
Opens your fish inventory.
|
||||
Your inventory contains all items you've purchased but not spent.
|
||||
ex:
|
||||
- ''
|
||||
params:
|
||||
- { }
|
||||
fishbuy:
|
||||
desc: |-
|
||||
Purchase a fishing item with the specified id.
|
||||
After purchase the item will appear in your inventory where you can use/equip it.
|
||||
ex:
|
||||
- '1'
|
||||
params:
|
||||
- id:
|
||||
desc: "The ID of the item to buy."
|
||||
fishuse:
|
||||
desc: |-
|
||||
Use a fishing item in your inventory.
|
||||
You can unequip it later, unless its a potion.
|
||||
ex:
|
||||
- '1'
|
||||
params:
|
||||
- index:
|
||||
desc: "The index of the item to use."
|
||||
fishunequip:
|
||||
desc: |-
|
||||
Unequips an item by specifying its index in your inventory.
|
||||
You can use it again later.
|
||||
You can't unequip potions.
|
||||
ex:
|
||||
- '1'
|
||||
params:
|
||||
- index:
|
||||
desc: "The index of the item to unequip."
|
|
@ -1248,5 +1248,16 @@
|
|||
"quest_log": "Quest Log",
|
||||
"dailies_done": "You've completed your dailies!",
|
||||
"dailies_reset": "Reset {0}",
|
||||
"daily_completed": "You've completed a daily quest: {0}"
|
||||
"daily_completed": "You've completed a daily quest: {0}",
|
||||
"fish_items_title": "Fishing Shop",
|
||||
"fish_buy_success": "Item purchased successfully!",
|
||||
"fish_item_not_found": "Item not found.",
|
||||
"fish_buy_insufficient_funds": "You don't have enough currency to buy this item.",
|
||||
"fish_buy_error": "An error occurred while trying to buy the item.",
|
||||
"fish_use_success": "Item equipped successfully!",
|
||||
"fish_use_error": "Unknown error occurred while trying to equip the item.",
|
||||
"fish_unequip_success": "Item unequipped successfully!",
|
||||
"fish_unequip_error": "Could not unequip item.",
|
||||
"fish_inv_title": "Fishing Inventory",
|
||||
"fish_cant_uneq_potion": "You can't unequip a potion."
|
||||
}
|
Loading…
Add table
Reference in a new issue