module Degenz.HackerBattle open System open System.Threading.Tasks open DSharpPlus open DSharpPlus.Entities open DSharpPlus.EventArgs open DSharpPlus.SlashCommands open Degenz open Degenz.Messaging // TODO: Do not allow any attacks until the user has completed training // TODO: Introduce second round of weapons, more expensive and with better stats let checkPlayerIsAttackingThemselves defender attacker = match attacker.DiscordId = defender.DiscordId with | true -> Error "You think you're clever? You can't hack yourself, pal." | false -> Ok attacker let checkAlreadyHackedTarget defenderId attacker = attacker.Actions |> Player.getAttacksFlat |> Array.tryFind (fun (_,t,_) -> t.Id = defenderId) |> function | Some ( atk , target , _ ) -> let cooldown = getTimeText true Game.SameTargetAttackCooldown atk.Timestamp Error $"You can only hack the same target once every {Game.SameTargetAttackCooldown.Hours} hours, wait {cooldown} to attempt another hack on {target.Name}." | None -> Ok attacker let checkItemHasCooldown itemId attacker = let cooldown = attacker.Actions |> Array.tryFind (fun a -> a.ActionId = itemId) |> function | Some a -> a.Timestamp | None -> DateTime.MinValue let item = Armory.getItem itemId if DateTime.UtcNow - cooldown > TimeSpan.FromMinutes(int item.Cooldown) then Ok attacker else let cooldown = getTimeText true (TimeSpan.FromMinutes(int item.Cooldown)) cooldown let item = Armory.battleItems |> Array.find (fun i -> i.Id = itemId) Error $"{item.Name} is currently on cooldown, wait {cooldown} to use it again." let checkHasEmptyHacks attacker = match Player.getHacks attacker with | [||] -> Error $"You currently do not have any Hacks to steal 💰$GBT from others. Please go to the <#{GuildEnvironment.channelArmory}> and purchase one." | _ -> Ok attacker let checkPlayerOwnsWeapon itemId player = match player.Arsenal |> Array.exists (fun i -> i.Id = itemId) with | true -> Ok player | false -> Error $"You sold your weapon already, you cheeky bastard..." let checkTargetHasMoney (target : PlayerData) attacker = if target.Bank < Game.HackPrize then Error $"{target.Name} does not have enough 💰$GBT to steal from, the broke loser. Pick a different target." else Ok attacker let checkPlayerHasShieldSlotsAvailable shield player = let updatedPlayer = player |> Player.removeExpiredActions false let defenses = Player.getDefenses updatedPlayer match defenses |> Array.length >= 2 with | true -> let timestamp = defenses |> Array.rev |> Array.head |> fun a -> a.Timestamp // This should be the next expiring timestamp let cooldown = getTimeText true (TimeSpan.FromMinutes(int shield.Cooldown)) timestamp Error $"You are only allowed two shields at a time. Wait {cooldown} to add another shield" | false -> Ok updatedPlayer let calculateDamage (hack : BattleItem) (shield : BattleItem) = if hack.Class = shield.Class then Weak else Strong let runHackerBattle defender hack = defender |> Player.removeExpiredActions false |> Player.getDefenses |> Array.map (fun dfn -> Armory.battleItems |> Array.find (fun w -> w.Id = dfn.ActionId)) |> Array.map (calculateDamage (hack)) |> Array.contains Weak let updateCombatants attacker defender hack prize = let updatePlayer amount attack p = { p with Actions = Array.append [| attack |] p.Actions ; Bank = max (p.Bank + amount) 0 } let target = { Id = defender.DiscordId ; Name = defender.Name } let attack = { ActionId = int hack ; Type = Attack { Target = target ; Result = prize > 0 } ; Timestamp = DateTime.UtcNow } [ DbService.updatePlayer <| updatePlayer prize attack attacker DbService.updatePlayer <| Player.modifyBank defender -prize ] |> Async.Parallel |> Async.Ignore let successfulHack (event : ComponentInteractionCreateEventArgs) attacker defender hack = async { do! updateCombatants attacker defender hack Game.HackPrize let embed = Embeds.responseSuccessfulHack true defender.DiscordId (Armory.getItem hack) do! event.Interaction.CreateFollowupMessageAsync(embed) |> Async.AwaitTask |> Async.Ignore let builder = Embeds.eventSuccessfulHack event defender Game.HackPrize let channel = event.Guild.GetChannel(GuildEnvironment.channelEventsHackerBattle) do! channel.SendMessageAsync(builder) |> Async.AwaitTask |> Async.Ignore } let failedHack (event : ComponentInteractionCreateEventArgs) attacker defender hack = async { let msg = $"Hack failed! {defender.Name} was able to mount a successful defense! You lost {Game.ShieldPrize} $GBT!" do! sendFollowUpMessage event msg do! updateCombatants attacker defender hack -Game.ShieldPrize let builder = DiscordMessageBuilder() builder.WithContent($"Hacking attempt failed! <@{defender.DiscordId}> defended hack from {event.User.Username} and stole {Game.ShieldPrize} $GBT from them! ") |> ignore let channel = (event.Guild.GetChannel(GuildEnvironment.channelEventsHackerBattle)) do! channel.SendMessageAsync(builder) |> Async.AwaitTask |> Async.Ignore } let attack (target : DiscordUser) (ctx : InteractionContext) = Game.executePlayerInteraction ctx (fun attacker -> async { let! defender = DbService.tryFindPlayer target.Id match defender with | Some defender -> do! attacker |> checkAlreadyHackedTarget defender.DiscordId (Player.removeExpiredActions true) >>= checkHasEmptyHacks >>= checkTargetHasMoney defender >>= checkPlayerIsAttackingThemselves defender |> function | Ok atkr -> let embed = Embeds.pickHack "Attack" atkr defender false ctx.FollowUpAsync(embed) |> Async.AwaitTask |> Async.Ignore | Error msg -> sendFollowUpMessageFromCtx ctx msg | None -> if target.IsBot then do! sendFollowUpMessageFromCtx ctx $"{target.Username} is a bot, pick a real human to hack" else do! sendFollowUpMessageFromCtx ctx "Your target is not connected to the network, they must join first by using the /redpill command" }) let handleAttack (event : ComponentInteractionCreateEventArgs) = Game.executePlayerEvent event (fun attacker -> async { let split = event.Id.Split("-") let hackId = int split.[1] let hack = enum(hackId) let ( resultId , targetId ) = UInt64.TryParse split.[2] let! resultTarget = DbService.tryFindPlayer targetId match resultTarget , true , resultId with | Some defender , true , true -> do! attacker |> Player.removeExpiredActions false |> checkAlreadyHackedTarget defender.DiscordId >>= checkPlayerOwnsWeapon hackId >>= checkItemHasCooldown hackId |> function | Ok atkr -> runHackerBattle defender (Armory.getItem (int hackId)) |> function | false -> successfulHack event atkr defender hackId | true -> failedHack event attacker defender hackId | Error msg -> Messaging.sendFollowUpMessage event msg | _ -> do! Messaging.sendFollowUpMessage event "Error occurred processing attack" }) let defend (ctx : InteractionContext) = Game.executePlayerInteraction ctx (fun player -> async { if Player.getShields player |> Array.length > 0 then let p = Player.removeExpiredActions false player let embed = Embeds.pickDefense "Defend" p false do! ctx.FollowUpAsync(embed) |> Async.AwaitTask |> Async.Ignore else let msg = $"You currently do not have any Shields to protect yourself from hacks. Please go to the <#{GuildEnvironment.channelArmory}> and purchase one." do! Messaging.sendFollowUpMessageFromCtx ctx msg }) let handleDefense (event : ComponentInteractionCreateEventArgs) = Game.executePlayerEvent event (fun player -> async { let split = event.Id.Split("-") let shieldId = int split.[1] let shield = Armory.getItem shieldId do! player |> checkPlayerOwnsWeapon shieldId >>= checkPlayerHasShieldSlotsAvailable shield >>= checkItemHasCooldown shieldId |> handleResultWithResponseFromEvent event (fun p -> async { let embed = Embeds.responseCreatedShield (Armory.getItem shieldId) do! event.Interaction.CreateFollowupMessageAsync(embed) |> Async.AwaitTask |> Async.Ignore let defense = { ActionId = shieldId ; Type = Defense ; Timestamp = DateTime.UtcNow } do! DbService.updatePlayer <| { player with Actions = Array.append [| defense |] player.Actions } 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 }) }) let handleButtonEvent (_ : DiscordClient) (event : ComponentInteractionCreateEventArgs) = match event.Id with | id when id.StartsWith("Attack") -> handleAttack event | id when id.StartsWith("Defend") -> handleDefense event | id when id.StartsWith("Trainer") -> Trainer.handleButtonEvent event |> Async.StartAsTask :> Task | _ -> task { let builder = DiscordInteractionResponseBuilder() builder.IsEphemeral <- true builder.Content <- $"Incorrect Action identifier {event.Id}" do! event.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, builder) |> Async.AwaitTask } let arsenal (ctx : InteractionContext) = Game.executePlayerInteraction ctx (fun player -> async { let updatedPlayer = Player.removeExpiredActions false player let builder = DiscordFollowupMessageBuilder() let embed = DiscordEmbedBuilder() embed.AddField("Arsenal", Arsenal.statusFormat updatedPlayer) |> ignore builder.AddEmbed(embed) |> ignore builder.IsEphemeral <- true do! ctx.FollowUpAsync(builder) |> Async.AwaitTask |> Async.Ignore do! DbService.updatePlayer updatedPlayer }) type HackerGame() = inherit ApplicationCommandModule () let enforceChannels (ctx : InteractionContext) (trainerFn : InteractionContext -> Task) (battleFn : InteractionContext -> Task) = match ctx.Channel.Id with | id when id = GuildEnvironment.channelTraining -> let hasTraineeRole = Seq.exists (fun (r : DiscordRole) -> r.Id = GuildEnvironment.roleTrainee) ctx.Member.Roles if hasTraineeRole then trainerFn ctx else task { let msg = $"You're currently not in training, either go to <#{GuildEnvironment.channelBattle}> to hack for real, " + "or restart training by clicking on the Pinned embed's button up top." do! Messaging.sendSimpleResponse ctx msg } | id when id = GuildEnvironment.channelBattle -> battleFn ctx | _ -> task { let msg = $"You must go to <#{GuildEnvironment.channelBattle}> channel to hack, shield, or check your arsenal" do! Messaging.sendSimpleResponse ctx msg } [] member this.Arsenal (ctx : InteractionContext) = enforceChannels ctx (Trainer.handleArsenal) arsenal [] member this.AttackCommand (ctx : InteractionContext, [] target : DiscordUser) = enforceChannels ctx (Trainer.attack target) (attack target) [] member this.ShieldCommand (ctx : InteractionContext) = enforceChannels ctx Trainer.defend defend // [] member this.TestAutoComplete (ctx : InteractionContext) = async { let builder = DiscordInteractionResponseBuilder() builder.IsEphemeral <- true do! ctx.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, builder) |> Async.AwaitTask } |> Async.StartAsTask :> Task