diff --git a/Cocotte/Cocotte.csproj b/Cocotte/Cocotte.csproj index 347f228..55e9e2a 100644 --- a/Cocotte/Cocotte.csproj +++ b/Cocotte/Cocotte.csproj @@ -11,4 +11,8 @@ + + + + diff --git a/Cocotte/Modules/PingModule.cs b/Cocotte/Modules/Ping/PingModule.cs similarity index 99% rename from Cocotte/Modules/PingModule.cs rename to Cocotte/Modules/Ping/PingModule.cs index a52f8c7..2f82eaf 100644 --- a/Cocotte/Modules/PingModule.cs +++ b/Cocotte/Modules/Ping/PingModule.cs @@ -8,12 +8,13 @@ using Discord; using Discord.Interactions; using Discord.WebSocket; -namespace Cocotte.Modules; +namespace Cocotte.Modules.Ping; /// /// Module containing different test and debug commands /// [RequireOwner] +[Group("ping", "Debug related commands")] public class PingModule : InteractionModuleBase { private readonly ILogger _logger; @@ -291,7 +292,7 @@ public class FoodModal : IModal [ModalTextInput("food_name", placeholder: "Pizza", maxLength: 20)] public string? Food { get; set; } - // Additional paremeters can be specified to further customize the input. + // Additional paremeters can be specified to further customize the input. // Parameters can be optional [RequiredInput(false)] [InputLabel("Why??")] diff --git a/Cocotte/Modules/Raids/IRaidsRepository.cs b/Cocotte/Modules/Raids/IRaidsRepository.cs new file mode 100644 index 0000000..17529d2 --- /dev/null +++ b/Cocotte/Modules/Raids/IRaidsRepository.cs @@ -0,0 +1,8 @@ +namespace Cocotte.Modules.Raids; + +public interface IRaidsRepository +{ + Raid this[ulong raidId] { get; } + + bool AddNewRaid(ulong raidId, DateTime dateTime); +} \ No newline at end of file diff --git a/Cocotte/Modules/Raids/MemoryRaidRepository.cs b/Cocotte/Modules/Raids/MemoryRaidRepository.cs new file mode 100644 index 0000000..3fd40b7 --- /dev/null +++ b/Cocotte/Modules/Raids/MemoryRaidRepository.cs @@ -0,0 +1,18 @@ +namespace Cocotte.Modules.Raids; + +public class MemoryRaidRepository : IRaidsRepository +{ + private readonly Dictionary _raids; + + public Raid this[ulong raidId] => _raids[raidId]; + + public MemoryRaidRepository() + { + _raids = new Dictionary(); + } + + public bool AddNewRaid(ulong raidId, DateTime dateTime) + { + return _raids.TryAdd(raidId, new Raid(raidId, dateTime)); + } +} \ No newline at end of file diff --git a/Cocotte/Modules/Raids/Raid.cs b/Cocotte/Modules/Raids/Raid.cs new file mode 100644 index 0000000..1332cc2 --- /dev/null +++ b/Cocotte/Modules/Raids/Raid.cs @@ -0,0 +1,38 @@ +using Cocotte.Utils; + +namespace Cocotte.Modules.Raids; + +public class Raid +{ + public ulong Id { get; } + public DateTime DateTime { get; } + + private readonly RosterManager _rosterManager = new(); + + public IEnumerable> Rosters => _rosterManager.Rosters; + + public Raid(ulong id, DateTime dateTime) + { + Id = id; + DateTime = dateTime; + +#if DEBUG + this.AddTestPlayers(); +#endif + } + + public bool AddPlayer(string name, PlayerRole role, int fc, bool substitute = false) + { + return _rosterManager.AddPlayer(new RosterPlayer(name, role, fc, substitute)); + } + + public override bool Equals(object? other) + { + return other is Raid roster && roster.Id == Id; + } + + public override int GetHashCode() + { + return (int) (Id % int.MaxValue); + } +} \ No newline at end of file diff --git a/Cocotte/Modules/Raids/RaidModule.cs b/Cocotte/Modules/Raids/RaidModule.cs new file mode 100644 index 0000000..476a0c5 --- /dev/null +++ b/Cocotte/Modules/Raids/RaidModule.cs @@ -0,0 +1,79 @@ +using System.Diagnostics.CodeAnalysis; +using Cocotte.Utils; +using Discord; +using Discord.Interactions; + +namespace Cocotte.Modules.Raids; + +[Group("raid", "Raid related commands")] +public class RaidModule : InteractionModuleBase +{ + private readonly ILogger _logger; + private readonly IRaidsRepository _raidsRepository; + + public RaidModule(ILogger logger, IRaidsRepository raidsRepository) + { + _logger = logger; + _raidsRepository = raidsRepository; + } + + [SlashCommand("start", "Start a raid formation")] + public async Task Ping() + { + // Raids are identified using their original message id + await RespondAsync("`Creating a new raid...`"); + + var response = await GetOriginalResponseAsync(); + var raidId = response.Id; + + _logger.LogInformation("Created new raid with id {RaidId}", raidId); + + // New raid instance + // TODO: Ask for date + if (!_raidsRepository.AddNewRaid(raidId, DateTime.Now)) + { + // A raid with this message id already exists, how?? + _logger.LogWarning("Tried to create a new raid with already existing id: {RaidId}", raidId); + + await FollowupAsync(ephemeral: true, embed: EmbedUtils.ErrorEmbed("Can't create a new raid with same raid id").Build()); + await DeleteOriginalResponseAsync(); + + return; + } + + // Build the raid message + var embed = RaidEmbed(raidId); + + await ModifyOriginalResponseAsync(m => + { + m.Content = ""; + m.Embed = embed.Build(); + }); + } + + [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")] + private EmbedBuilder RaidEmbed(ulong raidId) + { + EmbedFieldBuilder RosterEmbed(int rosterNumber, IEnumerable players) + { + var nonSubstitute = players.Where(p => !p.Substitue); + var substitute = players.Where(p => p.Substitue); + + var separatorLength = Math.Max(nonSubstitute.Select(p => p.Name.Length).Max(), substitute.Select(p => p.Name.Length).Max()); + separatorLength = (int) ((separatorLength + 13) * 0.49); // Don't ask why, it just works + + return new EmbedFieldBuilder() + .WithName($"Roster {rosterNumber}") + .WithValue($"{string.Join("\n", nonSubstitute)}\n{new string('━', separatorLength)}\n{string.Join("\n", substitute)}") + .WithIsInline(true); + } + + var raid = _raidsRepository[raidId]; + + return new EmbedBuilder() + .WithColor(Colors.CocotteBlue) + .WithTitle(":crossed_swords: Raid") + .WithDescription($"**Date:** {TimestampTag.FromDateTime(raid.DateTime, TimestampTagStyles.LongDateTime)}") + .WithFields(raid.Rosters.Select(r => RosterEmbed(r.Key, r))); + } +} \ No newline at end of file diff --git a/Cocotte/Modules/Raids/RosterManager.cs b/Cocotte/Modules/Raids/RosterManager.cs new file mode 100644 index 0000000..3d18ecc --- /dev/null +++ b/Cocotte/Modules/Raids/RosterManager.cs @@ -0,0 +1,16 @@ +namespace Cocotte.Modules.Raids; + +public class RosterManager +{ + private readonly ISet _rosters = new HashSet(); + + public IEnumerable> Rosters => _rosters.GroupBy(p => p.RosterNumber); + + public bool AddPlayer(RosterPlayer rosterPlayer) + { + // TODO add logic to split player in multiple rosters + rosterPlayer.RosterNumber = 1; + + return _rosters.Add(rosterPlayer); + } +} \ No newline at end of file diff --git a/Cocotte/Modules/Raids/RosterPlayer.cs b/Cocotte/Modules/Raids/RosterPlayer.cs new file mode 100644 index 0000000..773a368 --- /dev/null +++ b/Cocotte/Modules/Raids/RosterPlayer.cs @@ -0,0 +1,33 @@ +namespace Cocotte.Modules.Raids; + +public record RosterPlayer(string Name, PlayerRole Role, int Fc, bool Substitue = false) +{ + public int RosterNumber { get; set; } + + private static string RoleToEmote(PlayerRole role) => role switch + { + PlayerRole.Dps => ":red_circle:", + PlayerRole.Tank => ":yellow_circle:", + PlayerRole.Healer => ":green_circle:", + _ => ":question:" + }; + + public static string FcFormat(int fc) => fc switch + { + < 1_000 => $"{fc}", + _ => $"{fc/1000}k" + }; + + public override string ToString() => Substitue switch + { + false => $"{RoleToEmote(Role)} {Name} ({FcFormat(Fc)} FC)", + true => $"*{RoleToEmote(Role)} {Name} ({FcFormat(Fc)} FC)*" + }; +} + +public enum PlayerRole +{ + Dps, + Healer, + Tank +} \ No newline at end of file diff --git a/Cocotte/Program.cs b/Cocotte/Program.cs index 045fd0f..4b37408 100644 --- a/Cocotte/Program.cs +++ b/Cocotte/Program.cs @@ -1,3 +1,4 @@ +using Cocotte.Modules.Raids; using Cocotte.Options; using Cocotte.Services; using Discord; @@ -31,6 +32,9 @@ IHost host = Host.CreateDefaultBuilder(args) services.AddHostedService(); + // Data + services.AddSingleton(); + // Custom services.AddSingleton(); services.AddTransient(); diff --git a/Cocotte/Utils/Colors.cs b/Cocotte/Utils/Colors.cs new file mode 100644 index 0000000..8ed8929 --- /dev/null +++ b/Cocotte/Utils/Colors.cs @@ -0,0 +1,13 @@ +using Discord; + +namespace Cocotte.Utils; + +public static class Colors +{ + // Main Cocotte colors + public static Color CocotteBlue => new(0x3196c8); + + // Colors used in embeds + public static Color ErrorColor => new(0xFB6060); + public static Color InfoColor => new(0x66D9EF); +} \ No newline at end of file diff --git a/Cocotte/Utils/EmbedUtils.cs b/Cocotte/Utils/EmbedUtils.cs new file mode 100644 index 0000000..0277ec4 --- /dev/null +++ b/Cocotte/Utils/EmbedUtils.cs @@ -0,0 +1,28 @@ +using Discord; + +namespace Cocotte.Utils; + +public static class EmbedUtils +{ + public static EmbedBuilder ErrorEmbed(string message) + { + return new EmbedBuilder() + .WithColor(Colors.ErrorColor) + .WithAuthor(a => a + .WithName("Error") + .WithIconUrl("https://sage.cdn.ilysix.fr/assets/Cocotte/icons/error.webp") + ) + .WithDescription(message); + } + + public static EmbedBuilder InfoEmbed(string message) + { + return new EmbedBuilder() + .WithColor(Colors.InfoColor) + .WithAuthor(a => a + .WithName("Info") + .WithIconUrl("https://sage.cdn.ilysix.fr/assets/Cocotte/icons/info.webp") + ) + .WithDescription(message); + } +} \ No newline at end of file diff --git a/Cocotte/Utils/RaidExtensions.cs b/Cocotte/Utils/RaidExtensions.cs new file mode 100644 index 0000000..ba4e405 --- /dev/null +++ b/Cocotte/Utils/RaidExtensions.cs @@ -0,0 +1,14 @@ +using Cocotte.Modules.Raids; + +namespace Cocotte.Utils; + +public static class RaidExtensions +{ + public static void AddTestPlayers(this Raid raid) + { + raid.AddPlayer("YamaRaja", PlayerRole.Healer, 30000, false); + raid.AddPlayer("Zaku", PlayerRole.Dps, 40000, false); + raid.AddPlayer("Juchi", PlayerRole.Tank, 40000, false); + raid.AddPlayer("Akeno", PlayerRole.Dps, 40000, true); + } +} \ No newline at end of file