diff --git a/DiscordBot/Modules/UserSlashModule.cs b/DiscordBot/Modules/UserSlashModule.cs index 266256a5..495b77d5 100644 --- a/DiscordBot/Modules/UserSlashModule.cs +++ b/DiscordBot/Modules/UserSlashModule.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using Discord.Interactions; using DiscordBot.Services; using DiscordBot.Settings; @@ -13,6 +14,7 @@ public class UserSlashModule : InteractionModuleBase public CommandHandlingService CommandHandlingService { get; set; } public UserService UserService { get; set; } public BotSettings BotSettings { get; set; } + public ILoggingService LoggingService { get; set; } #endregion @@ -252,4 +254,277 @@ await Context.Interaction.ModifyOriginalResponseAsync(msg => } #endregion + + #region Duel System + + private static readonly ConcurrentDictionary _activeDuels = new ConcurrentDictionary(); + private static readonly Random _random = new Random(); + + private static readonly string[] _normalWinMessages = + { + "{winner} lands a solid hit on {loser} and wins the duel!", + "{winner} uses their sword to attack {loser}, but {loser} fails to dodge and {winner} wins!", + "{winner} outmaneuvers {loser} with a swift strike and claims victory!", + "{winner} blocks {loser}'s attack and counters with a decisive blow!", + "{winner} dodges {loser}'s clumsy swing and delivers the winning hit!", + "{winner} parries {loser}'s blade and strikes back to win the duel!", + "{winner} feints left, strikes right, and defeats {loser}!", + "{winner} overwhelms {loser} with superior technique and emerges victorious!" + }; + + [SlashCommand("duel", "Challenge another user to a duel!")] + public async Task Duel( + [Summary(description: "The user you want to duel")] IUser opponent, + [Summary(description: "Type of duel")] + [Choice("Normal", "normal")] + [Choice("Mute", "mute")] + string type = "normal") + { + // Prevent self-dueling + if (opponent.Id == Context.User.Id) + { + await Context.Interaction.RespondAsync("You cannot duel yourself!", ephemeral: true); + return; + } + + // Prevent dueling bots + if (opponent.IsBot) + { + await Context.Interaction.RespondAsync("You cannot duel a bot!", ephemeral: true); + return; + } + + // Check for active duel + string duelKey = $"{Context.User.Id}_{opponent.Id}"; + string reverseDuelKey = $"{opponent.Id}_{Context.User.Id}"; + + if (_activeDuels.ContainsKey(duelKey) || _activeDuels.ContainsKey(reverseDuelKey)) + { + await Context.Interaction.RespondAsync("There's already an active duel between you two!", ephemeral: true); + return; + } + + // Store the duel with both user IDs for timeout tracking + _activeDuels[duelKey] = (Context.User.Id, opponent.Id); + + var embed = new EmbedBuilder() + .WithColor(Color.Orange) + .WithTitle("⚔️ Duel Challenge!") + .WithDescription($"{Context.User.Mention} has challenged {opponent.Mention} to a {type} duel!") + .WithFooter($"This challenge will expire in 60 seconds") + .Build(); + + var components = new ComponentBuilder() + .WithButton("⚔️ Accept", $"duel_accept:{duelKey}:{type}", ButtonStyle.Success) + .WithButton("🛡️ Refuse", $"duel_refuse:{duelKey}", ButtonStyle.Danger) + .WithButton("❌ Cancel", $"duel_cancel:{duelKey}", ButtonStyle.Secondary) + .Build(); + + await Context.Interaction.RespondAsync(embed: embed, components: components); + + // Store the message reference for timeout + var originalResponse = await Context.Interaction.GetOriginalResponseAsync(); + + // Auto-timeout after 60 seconds + _ = Task.Run(async () => + { + await Task.Delay(60000); // 60 seconds + if (_activeDuels.ContainsKey(duelKey)) + { + var (challengerId, opponentId) = _activeDuels[duelKey]; + _activeDuels.TryRemove(duelKey, out _); + + try + { + var challenger = await Context.Guild.GetUserAsync(challengerId); + var challengedUser = await Context.Guild.GetUserAsync(opponentId); + + string timeoutMessage = challengedUser != null + ? $"⏰ Duel challenge to {challengedUser.Mention} expired." + : "⏰ Duel challenge expired."; + + await originalResponse.ModifyAsync(msg => + { + msg.Content = string.Empty; + msg.Embed = new EmbedBuilder() + .WithColor(Color.LightGrey) + .WithDescription(timeoutMessage) + .Build(); + msg.Components = new ComponentBuilder().Build(); + }); + } + catch (Exception ex) + { + await LoggingService.LogChannelAndFile($"Failed to modify duel timeout message: {ex.Message}", ExtendedLogSeverity.Warning); + } + } + }); + } + + [ComponentInteraction("duel_accept:*:*")] + public async Task DuelAccept(string duelKey, string type) + { + // Extract user IDs from the duel key + var userIds = duelKey.Split('_'); + if (userIds.Length != 2 || !ulong.TryParse(userIds[0], out var challengerId) || !ulong.TryParse(userIds[1], out var opponentId)) + { + await Context.Interaction.RespondAsync("Invalid duel data!", ephemeral: true); + return; + } + + // Only the challenged user can accept + if (Context.User.Id != opponentId) + { + await Context.Interaction.RespondAsync("Only the challenged user can accept this duel!", ephemeral: true); + return; + } + + // Check if duel is still active + if (!_activeDuels.ContainsKey(duelKey)) + { + await Context.Interaction.RespondAsync("This duel is no longer active!", ephemeral: true); + return; + } + + // Remove from active duels + _activeDuels.TryRemove(duelKey, out _); + + await Context.Interaction.DeferAsync(); + + // Get users + var challenger = await Context.Guild.GetUserAsync(challengerId); + var opponent = await Context.Guild.GetUserAsync(opponentId); + + if (challenger == null || opponent == null) + { + await Context.Interaction.FollowupAsync("One of the duel participants is no longer available!"); + return; + } + + // Randomly select winner (50/50) + bool challengerWins = _random.Next(2) == 0; + var winner = challengerWins ? challenger : opponent; + var loser = challengerWins ? opponent : challenger; + + // Generate flavor message + string flavorMessage = _normalWinMessages[_random.Next(_normalWinMessages.Length)]; + flavorMessage = flavorMessage.Replace("{winner}", winner.Mention).Replace("{loser}", loser.Mention); + + var resultEmbed = new EmbedBuilder() + .WithColor(Color.Gold) + .WithTitle("⚔️ Duel Results!") + .WithDescription(flavorMessage) + .AddField("Winner", winner.Mention, inline: true) + .Build(); + + await Context.Interaction.ModifyOriginalResponseAsync(msg => + { + msg.Embed = resultEmbed; + msg.Components = new ComponentBuilder().Build(); + }); + + // Handle mute duel using Discord timeout + if (type == "mute") + { + try + { + var guildLoser = loser as IGuildUser; + if (guildLoser != null) + { + // Use Discord's timeout feature for 10 minutes + await guildLoser.SetTimeOutAsync(TimeSpan.FromMinutes(10)); + await Context.Interaction.FollowupAsync($"💀 {loser.Mention} has been timed out for 10 minutes as the duel loser!", ephemeral: false); + } + } + catch (Exception ex) + { + await LoggingService.LogChannelAndFile($"Failed to timeout the loser of the duel: {ex.Message}", ExtendedLogSeverity.Error); + await Context.Interaction.FollowupAsync("Failed to timeout the loser.", ephemeral: false); + } + } + } + + [ComponentInteraction("duel_refuse:*")] + public async Task DuelRefuse(string duelKey) + { + // Extract user IDs from the duel key + var userIds = duelKey.Split('_'); + if (userIds.Length != 2 || !ulong.TryParse(userIds[0], out var challengerId) || !ulong.TryParse(userIds[1], out var opponentId)) + { + await Context.Interaction.RespondAsync("Invalid duel data!", ephemeral: true); + return; + } + + // Only the challenged user can refuse + if (Context.User.Id != opponentId) + { + await Context.Interaction.RespondAsync("Only the challenged user can refuse this duel!", ephemeral: true); + return; + } + + // Check if duel is still active + if (!_activeDuels.ContainsKey(duelKey)) + { + await Context.Interaction.RespondAsync("This duel is no longer active!", ephemeral: true); + return; + } + + // Remove from active duels + _activeDuels.TryRemove(duelKey, out _); + + // Edit the embed to show refusal instead of deleting + await Context.Interaction.DeferAsync(); + await Context.Interaction.ModifyOriginalResponseAsync(msg => + { + msg.Content = string.Empty; + msg.Embed = new EmbedBuilder() + .WithColor(Color.LightGrey) + .WithDescription("🛡️ Duel challenge was refused.") + .Build(); + msg.Components = new ComponentBuilder().Build(); + }); + } + + [ComponentInteraction("duel_cancel:*")] + public async Task DuelCancel(string duelKey) + { + // Extract user IDs from the duel key + var userIds = duelKey.Split('_'); + if (userIds.Length != 2 || !ulong.TryParse(userIds[0], out var challengerId) || !ulong.TryParse(userIds[1], out var opponentId)) + { + await Context.Interaction.RespondAsync("Invalid duel data!", ephemeral: true); + return; + } + + // Only the challenger can cancel + if (Context.User.Id != challengerId) + { + await Context.Interaction.RespondAsync("Only the challenger can cancel this duel!", ephemeral: true); + return; + } + + // Check if duel is still active + if (!_activeDuels.ContainsKey(duelKey)) + { + await Context.Interaction.RespondAsync("This duel is no longer active!", ephemeral: true); + return; + } + + // Remove from active duels + _activeDuels.TryRemove(duelKey, out _); + + // Edit the embed to show cancellation + await Context.Interaction.DeferAsync(); + await Context.Interaction.ModifyOriginalResponseAsync(msg => + { + msg.Content = string.Empty; + msg.Embed = new EmbedBuilder() + .WithColor(Color.LightGrey) + .WithDescription("❌ Duel challenge was cancelled by the challenger.") + .Build(); + msg.Components = new ComponentBuilder().Build(); + }); + } + + #endregion }