diff --git a/Cocotte/Modules/Raids/IRaidsRepository.cs b/Cocotte/Modules/Raids/IRaidsRepository.cs index 17529d2..70fb7bd 100644 --- a/Cocotte/Modules/Raids/IRaidsRepository.cs +++ b/Cocotte/Modules/Raids/IRaidsRepository.cs @@ -1,8 +1,12 @@ -namespace Cocotte.Modules.Raids; +using System.Diagnostics.CodeAnalysis; + +namespace Cocotte.Modules.Raids; public interface IRaidsRepository { Raid this[ulong raidId] { get; } bool AddNewRaid(ulong raidId, DateTime dateTime); + + bool TryGetRaid(ulong raidId, [MaybeNullWhen(false)] out Raid raid); } \ No newline at end of file diff --git a/Cocotte/Modules/Raids/MemoryRaidRepository.cs b/Cocotte/Modules/Raids/MemoryRaidRepository.cs index 3fd40b7..a5bf8a0 100644 --- a/Cocotte/Modules/Raids/MemoryRaidRepository.cs +++ b/Cocotte/Modules/Raids/MemoryRaidRepository.cs @@ -1,4 +1,6 @@ -namespace Cocotte.Modules.Raids; +using System.Diagnostics.CodeAnalysis; + +namespace Cocotte.Modules.Raids; public class MemoryRaidRepository : IRaidsRepository { @@ -15,4 +17,9 @@ public class MemoryRaidRepository : IRaidsRepository { return _raids.TryAdd(raidId, new Raid(raidId, dateTime)); } + + public bool TryGetRaid(ulong raidId, [MaybeNullWhen(false)] out Raid raid) + { + return _raids.TryGetValue(raidId, out raid); + } } \ No newline at end of file diff --git a/Cocotte/Modules/Raids/Raid.cs b/Cocotte/Modules/Raids/Raid.cs index 1332cc2..1129b9e 100644 --- a/Cocotte/Modules/Raids/Raid.cs +++ b/Cocotte/Modules/Raids/Raid.cs @@ -21,9 +21,19 @@ public class Raid #endif } - public bool AddPlayer(string name, PlayerRole role, int fc, bool substitute = false) + public bool AddPlayer(ulong id, string name, PlayerRole role, int fc, bool substitute = false) { - return _rosterManager.AddPlayer(new RosterPlayer(name, role, fc, substitute)); + return _rosterManager.AddPlayer(new RosterPlayer(id, name, role, fc, substitute)); + } + + public RosterPlayer GetPlayer(ulong id) + { + return _rosterManager.GetPlayer(id); + } + + public bool RemovePlayer(ulong id) + { + return _rosterManager.RemovePlayer(id); } public override bool Equals(object? other) @@ -35,4 +45,9 @@ public class Raid { return (int) (Id % int.MaxValue); } + + public override string ToString() + { + return $"Raid({DateTime})"; + } } \ No newline at end of file diff --git a/Cocotte/Modules/Raids/RaidModule.cs b/Cocotte/Modules/Raids/RaidModule.cs index 476a0c5..00ba9c4 100644 --- a/Cocotte/Modules/Raids/RaidModule.cs +++ b/Cocotte/Modules/Raids/RaidModule.cs @@ -2,10 +2,14 @@ using Cocotte.Utils; using Discord; using Discord.Interactions; +using Discord.WebSocket; + +// ReSharper disable UnusedMember.Global namespace Cocotte.Modules.Raids; [Group("raid", "Raid related commands")] +[SuppressMessage("Performance", "CA1822:Mark members as static")] public class RaidModule : InteractionModuleBase { private readonly ILogger _logger; @@ -17,8 +21,9 @@ public class RaidModule : InteractionModuleBase _raidsRepository = raidsRepository; } + [EnabledInDm(false)] [SlashCommand("start", "Start a raid formation")] - public async Task Ping() + public async Task Start() { // Raids are identified using their original message id await RespondAsync("`Creating a new raid...`"); @@ -42,22 +47,82 @@ public class RaidModule : InteractionModuleBase } // Build the raid message - var embed = RaidEmbed(raidId); + var raid = _raidsRepository[raidId]; + var embed = RaidEmbed(raid); + var components = RaidComponents(raidId); await ModifyOriginalResponseAsync(m => { m.Content = ""; m.Embed = embed.Build(); + m.Components = components.Build(); }); } - [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")] - private EmbedBuilder RaidEmbed(ulong raidId) + [ComponentInteraction("raid raid_join:*", true)] + public async Task Join(ulong raidId) + { + if (!_raidsRepository.TryGetRaid(raidId, out var raid)) + { + await RespondAsync(ephemeral: true, embed: EmbedUtils.ErrorEmbed("This raid does not exist").Build()); + + return; + } + + // Todo: Ask role, FC, substitute + var user = (IGuildUser)Context.User; + if (!raid.AddPlayer(user.Id, user.DisplayName, PlayerRole.Dps, 10000)) + { + await RespondAsync(ephemeral: true, embed: EmbedUtils.InfoEmbed("You're already registered for this raid").Build()); + + return; + } + + _logger.LogInformation("User {User} joined raid {Raid}", Context.User.Username, raid); + + await UpdateRaidRosterEmbed(raid); + await RespondAsync(ephemeral: true, embed: EmbedUtils.SuccessEmbed($"Successfully joined the raid as {raid.GetPlayer(user.Id).Role}").Build()); + } + + [ComponentInteraction("raid raid_leave:*", ignoreGroupNames: true)] + public async Task Leave(ulong raidId) + { + if (!_raidsRepository.TryGetRaid(raidId, out var raid)) + { + await RespondAsync(ephemeral: true, embed: EmbedUtils.ErrorEmbed("This raid does not exist").Build()); + + return; + } + + var user = (IGuildUser)Context.User; + if (!raid.RemovePlayer(user.Id)) + { + await RespondAsync(ephemeral: true, embed: EmbedUtils.WarningEmbed("You're not registered for this raid").Build()); + + return; + } + + await UpdateRaidRosterEmbed(raid); + await RespondAsync(ephemeral: true, embed: EmbedUtils.SuccessEmbed("Successfully left the raid").Build()); + } + + public async Task UpdateRaidRosterEmbed(Raid raid) + { + var message = await Context.Channel.GetMessageAsync(raid.Id); + + if (message is SocketUserMessage userMessage) + { + await userMessage.ModifyAsync(m => m.Embed = RaidEmbed(raid).Build()); + } + } + + private EmbedBuilder RaidEmbed(Raid raid) { EmbedFieldBuilder RosterEmbed(int rosterNumber, IEnumerable players) { - var nonSubstitute = players.Where(p => !p.Substitue); - var substitute = players.Where(p => p.Substitue); + var rosterPlayers = players.ToList(); + var nonSubstitute = rosterPlayers.Where(p => !p.Substitue); + var substitute = rosterPlayers.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 @@ -68,12 +133,27 @@ public class RaidModule : InteractionModuleBase .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))); } + + private ComponentBuilder RaidComponents(ulong raidId) + { + return new ComponentBuilder() + .AddRow(new ActionRowBuilder() + .WithButton(new ButtonBuilder() + .WithLabel("Join") + .WithCustomId($"raid raid_join:{raidId}") + .WithStyle(ButtonStyle.Primary) + ) + .WithButton(new ButtonBuilder() + .WithLabel("Leave") + .WithCustomId($"raid raid_leave:{raidId}") + .WithStyle(ButtonStyle.Danger) + ) + ); + } } \ No newline at end of file diff --git a/Cocotte/Modules/Raids/RosterManager.cs b/Cocotte/Modules/Raids/RosterManager.cs index 3d18ecc..af8643b 100644 --- a/Cocotte/Modules/Raids/RosterManager.cs +++ b/Cocotte/Modules/Raids/RosterManager.cs @@ -2,15 +2,25 @@ public class RosterManager { - private readonly ISet _rosters = new HashSet(); + private readonly IDictionary _players = new Dictionary(); - public IEnumerable> Rosters => _rosters.GroupBy(p => p.RosterNumber); + public IEnumerable> Rosters => _players.Select(p => p.Value).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); + return _players.TryAdd(rosterPlayer.Id, rosterPlayer); + } + + public bool RemovePlayer(ulong id) + { + return _players.Remove(id); + } + + public RosterPlayer GetPlayer(ulong id) + { + return _players[id]; } } \ No newline at end of file diff --git a/Cocotte/Modules/Raids/RosterPlayer.cs b/Cocotte/Modules/Raids/RosterPlayer.cs index 773a368..7d120ea 100644 --- a/Cocotte/Modules/Raids/RosterPlayer.cs +++ b/Cocotte/Modules/Raids/RosterPlayer.cs @@ -1,6 +1,6 @@ namespace Cocotte.Modules.Raids; -public record RosterPlayer(string Name, PlayerRole Role, int Fc, bool Substitue = false) +public record RosterPlayer(ulong Id, string Name, PlayerRole Role, int Fc, bool Substitue = false) { public int RosterNumber { get; set; } @@ -18,6 +18,16 @@ public record RosterPlayer(string Name, PlayerRole Role, int Fc, bool Substitue _ => $"{fc/1000}k" }; + public override int GetHashCode() + { + return (int) (Id % int.MaxValue); + } + + public virtual bool Equals(RosterPlayer? other) + { + return other is not null && other.Id == Id; + } + public override string ToString() => Substitue switch { false => $"{RoleToEmote(Role)} {Name} ({FcFormat(Fc)} FC)", diff --git a/Cocotte/Utils/Colors.cs b/Cocotte/Utils/Colors.cs index 8ed8929..c86df71 100644 --- a/Cocotte/Utils/Colors.cs +++ b/Cocotte/Utils/Colors.cs @@ -10,4 +10,6 @@ public static class Colors // Colors used in embeds public static Color ErrorColor => new(0xFB6060); public static Color InfoColor => new(0x66D9EF); + public static Color SuccessColor => new(0x2Ecc71); + public static Color WarningColor => new(0xf1c40F); } \ No newline at end of file diff --git a/Cocotte/Utils/EmbedUtils.cs b/Cocotte/Utils/EmbedUtils.cs index 0277ec4..f1a06fe 100644 --- a/Cocotte/Utils/EmbedUtils.cs +++ b/Cocotte/Utils/EmbedUtils.cs @@ -4,25 +4,47 @@ namespace Cocotte.Utils; public static class EmbedUtils { - public static EmbedBuilder ErrorEmbed(string message) + public static EmbedBuilder ErrorEmbed(string message, string title = "Error") { return new EmbedBuilder() .WithColor(Colors.ErrorColor) .WithAuthor(a => a - .WithName("Error") + .WithName(title) .WithIconUrl("https://sage.cdn.ilysix.fr/assets/Cocotte/icons/error.webp") ) .WithDescription(message); } - public static EmbedBuilder InfoEmbed(string message) + public static EmbedBuilder InfoEmbed(string message, string title = "Info") { return new EmbedBuilder() .WithColor(Colors.InfoColor) .WithAuthor(a => a - .WithName("Info") + .WithName(title) .WithIconUrl("https://sage.cdn.ilysix.fr/assets/Cocotte/icons/info.webp") ) .WithDescription(message); } + + public static EmbedBuilder SuccessEmbed(string message, string title = "Success") + { + return new EmbedBuilder() + .WithColor(Colors.SuccessColor) + .WithAuthor(a => a + .WithName(title) + .WithIconUrl("https://sage.cdn.ilysix.fr/assets/Cocotte/icons/success.webp") + ) + .WithDescription(message); + } + + public static EmbedBuilder WarningEmbed(string message, string title = "Warning") + { + return new EmbedBuilder() + .WithColor(Colors.WarningColor) + .WithAuthor(a => a + .WithName(title) + .WithIconUrl("https://sage.cdn.ilysix.fr/assets/Cocotte/icons/warning.webp") + ) + .WithDescription(message); + } } \ No newline at end of file diff --git a/Cocotte/Utils/RaidExtensions.cs b/Cocotte/Utils/RaidExtensions.cs index ba4e405..97a19ca 100644 --- a/Cocotte/Utils/RaidExtensions.cs +++ b/Cocotte/Utils/RaidExtensions.cs @@ -6,9 +6,9 @@ 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); + raid.AddPlayer(0, "YamaRaja", PlayerRole.Healer, 30000, false); + raid.AddPlayer(1, "Zaku", PlayerRole.Dps, 40000, false); + raid.AddPlayer(2, "Juchi", PlayerRole.Tank, 40000, false); + raid.AddPlayer(3, "Akeno", PlayerRole.Dps, 40000, true); } } \ No newline at end of file