From ed68f0b87de018142841a02b8389a2a41a7cae2a Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Mon, 24 Jan 2022 01:37:46 +0700 Subject: [PATCH] Implement new hacker battle mechanics --- Bot/Bot.fs | 2 +- Bot/Embeds.fs | 24 +++- Bot/HackerBattle.fs | 287 +++++++++++++++++++++++--------------- Bot/PlayerInteractions.fs | 10 +- Shared/Shared.fs | 8 +- 5 files changed, 201 insertions(+), 130 deletions(-) diff --git a/Bot/Bot.fs b/Bot/Bot.fs index fb8c7a1..2c8c7b3 100644 --- a/Bot/Bot.fs +++ b/Bot/Bot.fs @@ -40,7 +40,7 @@ let storeBot = new DiscordClient(storeConfig) //let slotMachineBot = new DiscordClient(slotMachineConfig) //let clients = [| storeBot ; trainerBot ; hackerBattleBot ; playerInteractionsBot ; slotMachineBot |] -let clients = [| storeBot ; hackerBattleBot ; playerInteractionsBot |] +let clients = [| storeBot ; hackerBattleBot ; playerInteractionsBot |] let sc1 = playerInteractionsBot.UseSlashCommands() let sc3 = hackerBattleBot.UseSlashCommands() diff --git a/Bot/Embeds.fs b/Bot/Embeds.fs index b360615..e0245da 100644 --- a/Bot/Embeds.fs +++ b/Bot/Embeds.fs @@ -7,7 +7,7 @@ open AsciiTableFormatter let constructEmbed message = let builder = DiscordEmbedBuilder() - builder.Color <- Optional(DiscordColor.PhthaloGreen) + builder.Color <- Optional(DiscordColor.Blurple) builder.Description <- message let author = DiscordEmbedBuilder.EmbedAuthor() author.Name <- "Degenz Hacker Game" @@ -23,7 +23,7 @@ let pickDefense actionId player = let embed = DiscordEmbedBuilder() - .WithColor(DiscordColor.PhthaloGreen) + .WithColor(DiscordColor.Blurple) .WithDescription("Pick a defense to mount for 10 hours") .WithImageUrl("https://s10.gifyu.com/images/Defense-Degenz-V2.gif") @@ -39,7 +39,7 @@ let pickHack actionId attacker defender = let embed = DiscordEmbedBuilder() - .WithColor(DiscordColor.PhthaloGreen) + .WithColor(DiscordColor.Blurple) .WithDescription("Pick the hack that you want to use") .WithImageUrl("https://s10.gifyu.com/images/Hacker-Degenz-V2.gif") @@ -49,6 +49,22 @@ let pickHack actionId attacker defender = .AsEphemeral true let eventSuccessfulHack (event : ComponentInteractionCreateEventArgs) targetId prize = + let embed = + DiscordEmbedBuilder() + .WithColor(DiscordColor.Blurple) + .WithDescription("Pick the hack that you want to use") + .WithImageUrl("https://s10.gifyu.com/images/Hacker-Degenz-V2.gif") + + DiscordMessageBuilder() + .WithContent($"{event.User.Username} successfully hacked <@{targetId}> for a total of {prize} GoodBoyTokenz") + +let eventFailedHack (event : ComponentInteractionCreateEventArgs) targetId prize = + let embed = + DiscordEmbedBuilder() + .WithColor(DiscordColor.Blurple) + .WithDescription("Pick the hack that you want to use") + .WithImageUrl("https://s10.gifyu.com/images/Hacker-Degenz-V2.gif") + DiscordMessageBuilder() .WithContent($"{event.User.Username} successfully hacked <@{targetId}> for a total of {prize} GoodBoyTokenz") @@ -77,7 +93,7 @@ let storeListing store = |> getClass { Name = item.Name ; Cost = string item.Cost ; Class = string itemClass }) |> Formatter.Format - |> sprintf "**%A**\n``` %s ```" itemType + |> sprintf "**%As**\n``` %s ```" itemType |> constructEmbed) DiscordInteractionResponseBuilder() diff --git a/Bot/HackerBattle.fs b/Bot/HackerBattle.fs index 3d017c4..3cf8c58 100644 --- a/Bot/HackerBattle.fs +++ b/Bot/HackerBattle.fs @@ -9,124 +9,141 @@ open DSharpPlus.SlashCommands open Degenz open Degenz.Shared +let checkForExistingHack attacker defenderId = + let updatedAttacks = + attacker.Attacks + |> removeExpiredActions (TimeSpan.FromHours(24)) (fun atk -> atk.Timestamp) + updatedAttacks + |> Array.tryFind (fun a -> a.Target.Id = defenderId) + |> function + | Some attack -> + let timeRemaining = TimeSpan.FromHours(24) - (DateTime.UtcNow - attack.Timestamp) + Error $"You can only hack the same target once every 24 hours, wait {timeRemaining.Seconds} seconds to attempt another hack on {attack.Target.Name}." + | None -> + Ok updatedAttacks + +let checkIfHackHasCooldown hack updatedAttacks = + let mostRecentHackAttack = + updatedAttacks + |> Array.tryFind (fun a -> a.HackType = hack) + |> function + | Some a -> a.Timestamp + | None -> DateTime.UtcNow + if DateTime.UtcNow - mostRecentHackAttack <= TimeSpan.FromMinutes(5) then + Ok updatedAttacks + else + let timeRemaining = TimeSpan.FromMinutes(5) - (DateTime.UtcNow - mostRecentHackAttack) + Error $"You can only attack once a minute, wait {timeRemaining.Seconds} seconds to attack again." + +let calculateDamage (hack: int) (shield: int) = + let hackClass = getClass hack + let protectionClass = getClass shield + + match hackClass, protectionClass with + | h, p when h = p -> Weak + | _ -> Strong + +let runHackerBattle attacker defender hack = + defender.Defenses + |> removeExpiredActions (TimeSpan.FromHours(6)) (fun (pro : Defense) -> pro.Timestamp) + |> Seq.toArray + |> Array.map (fun dfn -> int dfn.DefenseType) + |> Array.map (calculateDamage (int hack)) + |> Array.contains Weak + +let updateCombatants attacker defender hack prize = + let updatePlayer amount attack p = + { p with Attacks = Array.append [| attack |] p.Attacks ; Bank = Math.Max(p.Bank + amount, 0) } + let attack = { HackType = enum(int hack) ; Timestamp = DateTime.UtcNow ; Target = { Id = defender.DiscordId ; Name = defender.Name } } + + [ DbService.updatePlayer <| updatePlayer prize attack attacker + DbService.updatePlayer <| modifyPlayerBank defender -prize ] + |> Async.Parallel + |> Async.Ignore + +let successfulHack (event : ComponentInteractionCreateEventArgs) attacker defender hack = + async { + let prize = 3 + + do! updateCombatants attacker defender hack prize + + let builder = DiscordInteractionResponseBuilder() + builder.IsEphemeral <- true + builder.Content <- $"Successfully hacked {defender.Name} using {hack}! You just won {prize} GoodBoyTokenz!" + // TODO: Don't make this an Update + do! event.Interaction.CreateResponseAsync(InteractionResponseType.UpdateMessage, builder) + |> Async.AwaitTask + + let builder = Embeds.eventSuccessfulHack event defender.DiscordId prize + let channel = event.Guild.GetChannel(GuildEnvironment.channelEventsHackerBattle) + do! channel.SendMessageAsync(builder) + |> Async.AwaitTask + |> Async.Ignore + } + +let failedHack (event : ComponentInteractionCreateEventArgs) attacker defender hack = + async { + let builder = DiscordInteractionResponseBuilder() + let prize = 2 + builder.IsEphemeral <- true + builder.Content <- $"Hack failed! {defender.Name} was able to mount a successful defense! You lost {prize} GoodBoyTokenz!" + do! event.Interaction.CreateResponseAsync(InteractionResponseType.UpdateMessage, builder) + |> Async.AwaitTask + + do! updateCombatants attacker defender hack -prize + + let builder = DiscordMessageBuilder() + builder.WithContent($"Hacking attempt failed! <@{defender.DiscordId}> defended hack from {event.User.Username} and took {prize} from them! ") |> ignore + let channel = (event.Guild.GetChannel(GuildEnvironment.channelEventsHackerBattle)) + do! channel.SendMessageAsync(builder) + |> Async.AwaitTask + |> Async.Ignore + } + let attack (ctx : InteractionContext) (target : DiscordUser) = async { let! attacker = DbService.tryFindPlayer ctx.Member.Id let! defender = DbService.tryFindPlayer target.Id match attacker , defender with | Some attacker , Some defender -> - let updatedAttacks = - attacker.Attacks - |> removeExpiredActions (TimeSpan.FromMinutes(15)) (fun (atk : Attack) -> atk.Timestamp) - do! DbService.updatePlayer <| { attacker with Attacks = updatedAttacks } - if updatedAttacks.Length < 2 then - let embed = Embeds.pickHack "Attack" attacker defender - - do! ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, embed) - |> Async.AwaitTask - - else - let builder = DiscordInteractionResponseBuilder() - let timestamp = updatedAttacks |> Array.rev |> Array.head |> fun a -> a.Timestamp // This should be the next expiring timestamp - let timeRemaining = TimeSpan.FromMinutes(15) - (DateTime.UtcNow - timestamp) - builder.Content <- $"No more hacks available, please wait {timeRemaining.Minutes} minutes and {timeRemaining.Seconds} seconds to attempt another hack" - - builder.AsEphemeral true |> ignore - - do! ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, builder) - |> Async.AwaitTask - + let existingHack = checkForExistingHack attacker defender.DiscordId + match existingHack with + | Ok _ -> + let embed = Embeds.pickHack "Attack" attacker defender + do! ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, embed) + |> Async.AwaitTask + | Error msg -> + let builder = DiscordInteractionResponseBuilder().WithContent(msg).AsEphemeral(true) + do! ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, builder) + |> Async.AwaitTask | None , _ -> do! notYetAHackerMsg ctx | _ , None -> do! createSimpleResponseAsync "Your target is not connected to the network, they must join first by using the /redpill command" ctx } |> Async.StartAsTask :> Task -let defend (ctx : InteractionContext) = - async { - let! player = DbService.tryFindPlayer ctx.Member.Id - match player with - | Some player -> - let updatedDefenses = removeExpiredActions (TimeSpan.FromHours(24)) (fun (pro : Defense) -> pro.Timestamp) player.Defenses - do! DbService.updatePlayer <| { player with Defenses = updatedDefenses } - if updatedDefenses.Length < 3 then - let embed = Embeds.pickDefense "Defend" player - - do! ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, embed) - |> Async.AwaitTask - else - let builder = DiscordInteractionResponseBuilder() - let timestamp = updatedDefenses |> Array.rev |> Array.head |> fun a -> a.Timestamp // This should be the next expiring timestamp - let timeRemaining = TimeSpan.FromMinutes(15) - (DateTime.UtcNow - timestamp) - // TODO: Make this handle hours and minutes - builder.Content <- $"Cannot add new defense, please wait {timeRemaining.Hours} hours and {timeRemaining.Minutes} minutes to add another defense" - - builder.AsEphemeral true |> ignore - - do! ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, builder) - |> Async.AwaitTask - | None -> do! notYetAHackerMsg ctx - } |> Async.StartAsTask - :> Task - let handleAttack (event : ComponentInteractionCreateEventArgs) = - let updatePlayer amount attack p = - { p with Attacks = Array.append [| attack |] p.Attacks ; Bank = Math.Max(p.Bank + amount, 0) } async { let split = event.Id.Split("-") - let weapon = Enum.Parse(typedefof, split.[1]) :?> Hack + let hack = Enum.Parse(typedefof, split.[1]) :?> Hack let ( resultId , targetId ) = UInt64.TryParse split.[2] let! resultPlayer = DbService.tryFindPlayer event.User.Id let! resultTarget = DbService.tryFindPlayer targetId + + // TODO: Do not let player hack themselves match resultPlayer , resultTarget , true , resultId with - | Some player , Some target , true , true -> - let updatedDefenses = removeExpiredActions (TimeSpan.FromHours(24)) (fun (p : Defense) -> p.Timestamp) target.Defenses - do! DbService.updatePlayer <| { player with Defenses = updatedDefenses } - let wasSuccessfulHack = - updatedDefenses - |> Seq.toArray - |> Array.map (fun dfn -> int dfn.DefenseType) - |> Array.map (calculateDamage (int weapon)) - |> Array.contains Weak - match wasSuccessfulHack with - | false -> -// let prize = 1.337f // LEET - let prize = 13 - let attack = { HackType = enum(int weapon) ; Timestamp = DateTime.UtcNow ; Target = { Id = targetId ; Name = split.[3] } } - - let! _ = - [ DbService.updatePlayer <| updatePlayer prize attack player - DbService.updatePlayer { target with Bank = Math.Max(target.Bank - prize, 0)} ] - |> Async.Parallel - - let builder = DiscordInteractionResponseBuilder() - builder.IsEphemeral <- true - builder.Content <- $"Successfully hacked {split.[3]} using {weapon}! You just won {prize} GoodBoyTokenz!" - do! event.Interaction.CreateResponseAsync(InteractionResponseType.UpdateMessage, builder) - |> Async.AwaitTask - - let builder = Embeds.eventSuccessfulHack event targetId prize - let channel = event.Guild.GetChannel(GuildEnvironment.channelEventsHackerBattle) - do! channel.SendMessageAsync(builder) - |> Async.AwaitTask - |> Async.Ignore - | true -> - let builder = DiscordInteractionResponseBuilder() - let prize = 2 - builder.IsEphemeral <- true - builder.Content <- $"Hack failed! {split.[3]} was able to mount a successful defense! You lost {prize} GoodBoyTokenz!" - do! event.Interaction.CreateResponseAsync(InteractionResponseType.UpdateMessage, builder) - |> Async.AwaitTask - - let attack = { HackType = enum(int weapon) ; Timestamp = DateTime.UtcNow ; Target = { Id = targetId ; Name = split.[3] } } - do! DbService.updatePlayer <| updatePlayer -prize attack player - do! DbService.updatePlayer { target with Bank = target.Bank + prize } - - let builder = DiscordMessageBuilder() - builder.WithContent($"Hacking attempt failed! <@{targetId}> defended hack from {event.User.Username} and took {prize} from them! ") |> ignore - let channel = (event.Guild.GetChannel(GuildEnvironment.channelEventsHackerBattle)) - do! channel.SendMessageAsync(builder) - |> Async.AwaitTask - |> Async.Ignore + | Some attacker , Some defender , true , true -> + do! checkForExistingHack attacker defender.DiscordId + |> Result.bind (checkIfHackHasCooldown hack) + |> function + | Ok _ -> + runHackerBattle attacker defender hack + |> function + | false -> successfulHack event attacker defender hack + | true -> failedHack event attacker defender hack + | Error msg -> + let builder = DiscordInteractionResponseBuilder() + event.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, builder.WithContent(msg)) + |> Async.AwaitTask | _ -> let builder = DiscordInteractionResponseBuilder() builder.IsEphemeral <- true @@ -135,6 +152,25 @@ let handleAttack (event : ComponentInteractionCreateEventArgs) = |> Async.AwaitTask } +let defend (ctx : InteractionContext) = + async { + let! player = DbService.tryFindPlayer ctx.Member.Id + match player with + | Some player -> + if player.Shields.Length > 0 then + let embed = Embeds.pickDefense "Defend" player + do! ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, embed) + |> Async.AwaitTask + else + let builder = DiscordInteractionResponseBuilder() + builder.Content <- $"You currently do not have any Shields to protect your system. Please go to the store and purchase one." + builder.AsEphemeral true |> ignore + do! ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, builder) + |> Async.AwaitTask + | None -> do! notYetAHackerMsg ctx + } |> Async.StartAsTask + :> Task + let handleDefense (event : ComponentInteractionCreateEventArgs) = async { let split = event.Id.Split("-") @@ -142,21 +178,46 @@ let handleDefense (event : ComponentInteractionCreateEventArgs) = let! playerResult = DbService.tryFindPlayer event.User.Id match playerResult , shieldResult with | Some player , true -> - let builder = DiscordInteractionResponseBuilder() - builder.IsEphemeral <- true - builder.Content <- $"Mounted a {shield} defense for 24 hours" - do! event.Interaction.CreateResponseAsync(InteractionResponseType.UpdateMessage, builder) - |> Async.AwaitTask + // TODO: All of this is wrong + let updatedDefenses = removeExpiredActions (TimeSpan.FromHours(6)) (fun (pro : Defense) -> pro.Timestamp) player.Defenses + let alreadyUsedShield = updatedDefenses |> Array.exists (fun d -> d.DefenseType = shield) - let defense = { DefenseType = shield ; Timestamp = DateTime.UtcNow } - do! DbService.updatePlayer <| { player with Defenses = Array.append [| defense |] player.Defenses } + match alreadyUsedShield , updatedDefenses.Length < 2 with + | false , true -> + let builder = DiscordInteractionResponseBuilder() + builder.IsEphemeral <- true + builder.Content <- $"Mounted a {shield} defense for 6 hours" + do! event.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, builder) + |> Async.AwaitTask + let defense = { DefenseType = shield ; Timestamp = DateTime.UtcNow } + do! DbService.updatePlayer <| { player with Defenses = Array.append [| defense |] player.Defenses } + let builder = DiscordMessageBuilder() + builder.WithContent($"{event.User.Username} has protected their system!") |> ignore + let channel = event.Guild.GetChannel(GuildEnvironment.channelEventsHackerBattle) + do! channel.SendMessageAsync(builder) + |> Async.AwaitTask + |> Async.Ignore + | _ , false -> + let builder = DiscordInteractionResponseBuilder() + builder.IsEphemeral <- true + let timestamp = updatedDefenses |> Array.rev |> Array.head |> fun a -> a.Timestamp // This should be the next expiring timestamp + let timeRemaining = TimeSpan.FromMinutes(15) - (DateTime.UtcNow - timestamp) + let hours = if timeRemaining.Hours > 0 then $"{timeRemaining.Hours} hours and " else "" + builder.Content <- $"You are only allowed two shields at a time. Wait {hours}{timeRemaining.Minutes} minutes to add another shield" + do! event.Interaction.CreateResponseAsync(InteractionResponseType.UpdateMessage, builder) + |> Async.AwaitTask + do! DbService.updatePlayer <| { player with Defenses = updatedDefenses } + | true , _ -> + let builder = DiscordInteractionResponseBuilder() + builder.IsEphemeral <- true + let timestamp = updatedDefenses |> Array.find (fun d -> d.DefenseType = shield) |> fun a -> a.Timestamp + let timeRemaining = TimeSpan.FromMinutes(15) - (DateTime.UtcNow - timestamp) + let hours = if timeRemaining.Hours > 0 then $"{timeRemaining.Hours} hours and " else "" + builder.Content <- $"{shield} shield is already in use. Wait {hours}{timeRemaining.Minutes} minutes to use this shield again" + do! event.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, builder) + |> Async.AwaitTask + do! DbService.updatePlayer <| { player with Defenses = updatedDefenses } - let builder = DiscordMessageBuilder() - builder.WithContent($"{event.User.Username} has protected their system!") |> ignore - let channel = event.Guild.Channels.Values |> Seq.find (fun c -> c.Name = "battle-1") - do! channel.SendMessageAsync(builder) - |> Async.AwaitTask - |> Async.Ignore | _ -> let builder = DiscordInteractionResponseBuilder() builder.IsEphemeral <- true diff --git a/Bot/PlayerInteractions.fs b/Bot/PlayerInteractions.fs index d9ceb29..8655de7 100644 --- a/Bot/PlayerInteractions.fs +++ b/Bot/PlayerInteractions.fs @@ -106,13 +106,13 @@ module Commands = let! player = DbService.tryFindPlayer ctx.Member.Id match player with | Some p -> - // TODO: Is this working? - let updatedAttacks = p.Attacks |> removeExpiredActions (TimeSpan.FromMinutes(15)) (fun (atk : Attack) -> atk.Timestamp) - let updatedDefenses = p.Defenses |> removeExpiredActions (TimeSpan.FromHours(24)) (fun (p : Defense) -> p.Timestamp) - do! DbService.updatePlayer <| { p with Attacks = updatedAttacks ; Defenses = updatedDefenses } + let updatedAttacks = p.Attacks |> removeExpiredActions (TimeSpan.FromHours(24)) (fun (atk : Attack) -> atk.Timestamp) + let updatedDefenses = p.Defenses |> removeExpiredActions (TimeSpan.FromHours(6)) (fun (p : Defense) -> p.Timestamp) + let updatedPlayer = { p with Attacks = updatedAttacks ; Defenses = updatedDefenses } + do! DbService.updatePlayer updatedPlayer let builder = DiscordInteractionResponseBuilder() builder.IsEphemeral <- true - builder.Content <- statusFormat p + builder.Content <- statusFormat updatedPlayer do! ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, builder) |> Async.AwaitTask | None -> do! notYetAHackerMsg ctx diff --git a/Shared/Shared.fs b/Shared/Shared.fs index 5861f51..ba5d66e 100644 --- a/Shared/Shared.fs +++ b/Shared/Shared.fs @@ -106,13 +106,7 @@ let constructButtons (actionType: string) (playerInfo: string) (weapons: 'a arra let removeExpiredActions timespan (timestamp: 'a -> DateTime) actions = actions |> Array.filter (fun act -> DateTime.UtcNow - (timestamp act) < timespan) -let calculateDamage (hack: int) (shield: int) = - let hackClass = getClass hack - let protectionClass = getClass shield - - match hackClass, protectionClass with - | h, p when h = p -> Weak - | _ -> Strong +let modifyPlayerBank player amount = { player with Bank = Math.Max(player.Bank + amount, 0) } module Message = let sendFollowUpMessage (event : ComponentInteractionCreateEventArgs) msg =