diff --git a/Bot/Embeds.fs b/Bot/Embeds.fs index 9825c58..2e77b1e 100644 --- a/Bot/Embeds.fs +++ b/Bot/Embeds.fs @@ -1,7 +1,6 @@ module Degenz.Embeds open System -open DSharpPlus open DSharpPlus.EventArgs open Degenz.Types open DSharpPlus.Entities @@ -33,18 +32,24 @@ let getShieldGif = function | ShieldId.Cypher -> "https://s10.gifyu.com/images/Cypher-Smaller.gif" | _ -> shieldGif -let constructButtons (actionType: string) (player: PlayerData) (buttonId : string) (items: BattleItem array) isTrainer = - items +let constructButtons (actionId: string) (buttonInfo : string) (player: PlayerData) itemType isTrainer = + player + |> Player.getItems itemType |> Array.map (fun item -> -// match player.Actions |> Array.exists (fun i -> i.ActionId = item.Id) && not isTrainer with - match false with - | true -> DiscordButtonComponent(Game.getClassButtonColor item.Class, $"{actionType}-{item.Id}", $"{item.Name} (on cooldown)", true) - | false -> DiscordButtonComponent(Game.getClassButtonColor item.Class, $"{actionType}-{item.Id}-{buttonId}", $"{item.Name}")) + let action = + player.Actions + |> Array.tryFind (fun i -> i.ActionId = item.Id) + match action , isTrainer with + | None , _ | Some _ , true -> + DiscordButtonComponent(Game.getClassButtonColor item.Class, $"{actionId}-{item.Id}-{buttonInfo}", $"{item.Name}") + | Some act , false -> + let c = ((Armory.getItem act.ActionId).Cooldown) + let time = Messaging.getShortTimeText (TimeSpan.FromMinutes(int c)) act.Timestamp + DiscordButtonComponent(Game.getClassButtonColor item.Class, $"{actionId}-{item.Id}", $"{item.Name} ({time} left)", true)) + |> Seq.cast let pickDefense actionId player isTrainer = - let buttons = - constructButtons actionId player (string player.DiscordId) (Player.shields player) isTrainer - |> Seq.cast + let buttons = constructButtons actionId (string player.DiscordId) player ItemType.Shield isTrainer let embed = DiscordEmbedBuilder() @@ -58,10 +63,7 @@ let pickDefense actionId player isTrainer = .AsEphemeral(true) let pickHack actionId attacker defender isTrainer = - let buttons = - let hacks = Player.hacks attacker - constructButtons actionId attacker $"{defender.DiscordId}-{defender.Name}" hacks isTrainer - |> Seq.cast + let buttons = constructButtons actionId $"{defender.DiscordId}-{defender.Name}" attacker ItemType.Hack isTrainer let embed = DiscordEmbedBuilder() diff --git a/Bot/Game.fs b/Bot/Game.fs index bebb0a9..fc5551b 100644 --- a/Bot/Game.fs +++ b/Bot/Game.fs @@ -53,23 +53,23 @@ module Game = :> Task module Player = - let hacks (player : PlayerData) = player.Arsenal |> Array.filter (fun bi -> bi.Type = Hack) - let shields (player : PlayerData) = player.Arsenal |> Array.filter (fun bi -> bi.Type = Shield) + let getItems itemType (player : PlayerData) = player.Arsenal |> Array.filter (fun i -> i.Type = itemType) + let hacks (player : PlayerData) = getItems ItemType.Hack player + let getShields (player : PlayerData) = getItems ItemType.Shield player let attacks player = player.Actions |> Array.filter (fun act -> match act.Type with Attack _ -> true | _ -> false) let defenses player = player.Actions |> Array.filter (fun act -> match act.Type with Defense -> true | _ -> false) - let removeExpiredActions player = + let removeExpiredActions filterByAttackCooldown player = let actions = player.Actions |> Array.filter (fun (act : Action) -> - match act.Type with - // So the player doesnt attack the same player so many times in a row - | Attack _ -> System.DateTime.UtcNow - act.Timestamp < Game.SameTargetAttackCooldown - | Defense -> - let item = Armory.getItem act.ActionId - System.DateTime.UtcNow - act.Timestamp < System.TimeSpan.FromMinutes(int item.Cooldown)) + let item = Armory.getItem act.ActionId + match act.Type , filterByAttackCooldown with + | Attack _ , true -> System.DateTime.UtcNow - act.Timestamp < System.TimeSpan.FromMinutes(int item.Cooldown) + | Attack _ , false -> System.DateTime.UtcNow - act.Timestamp < Game.SameTargetAttackCooldown + | Defense , _ -> System.DateTime.UtcNow - act.Timestamp < System.TimeSpan.FromMinutes(int item.Cooldown)) { player with Actions = actions } let modifyBank (player : PlayerData) amount = { player with Bank = max (player.Bank + amount) 0 } diff --git a/Bot/HackerBattle.fs b/Bot/HackerBattle.fs index e703ab4..2923b75 100644 --- a/Bot/HackerBattle.fs +++ b/Bot/HackerBattle.fs @@ -22,23 +22,23 @@ let checkAlreadyHackedTarget defenderId attacker = |> Array.tryFind (fun (_,t,_) -> t.Id = defenderId) |> function | Some ( atk , target , _ ) -> - let cooldown = getTimeTillCooldownFinishes Game.SameTargetAttackCooldown atk.Timestamp + 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 checkHackHasCooldown hackId attacker = - let mostRecentHackAttack = +let checkItemHasCooldown itemId attacker = + let cooldown = attacker.Actions - |> Array.tryFind (fun a -> a.ActionId = hackId) + |> Array.tryFind (fun a -> a.ActionId = itemId) |> function | Some a -> a.Timestamp | None -> DateTime.MinValue - let item = Armory.getItem hackId - if DateTime.UtcNow - mostRecentHackAttack > TimeSpan.FromMinutes(int item.Cooldown) then + let item = Armory.getItem itemId + if DateTime.UtcNow - cooldown > TimeSpan.FromMinutes(int item.Cooldown) then Ok attacker else - let cooldown = getTimeTillCooldownFinishes (TimeSpan.FromMinutes(int item.Cooldown)) mostRecentHackAttack - let item = Armory.battleItems |> Array.find (fun i -> i.Id = hackId) + 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 = @@ -46,11 +46,26 @@ let checkHasEmptyHacks attacker = | [||] -> 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 = updatedPlayer |> fun p -> p.Actions + 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 @@ -58,7 +73,7 @@ let calculateDamage (hack : BattleItem) (shield : BattleItem) = let runHackerBattle defender hack = defender - |> Player.removeExpiredActions + |> Player.removeExpiredActions false |> Player.defenses |> Array.map (fun dfn -> Armory.battleItems |> Array.find (fun w -> w.Id = dfn.ActionId)) |> Array.map (calculateDamage (hack)) @@ -79,7 +94,7 @@ let successfulHack (event : ComponentInteractionCreateEventArgs) attacker defend async { do! updateCombatants attacker defender hack Game.HackPrize - let embed = Embeds.responseSuccessfulHack (defender.Name) (Armory.getItem (int hack)) + let embed = Embeds.responseSuccessfulHack (defender.Name) (Armory.getItem hack) do! event.Interaction.CreateFollowupMessageAsync(embed) |> Async.AwaitTask |> Async.Ignore @@ -112,14 +127,14 @@ let attack (target : DiscordUser) (ctx : InteractionContext) = match defender with | Some defender -> do! attacker - |> Player.removeExpiredActions |> checkAlreadyHackedTarget defender.DiscordId + (Player.removeExpiredActions true) >>= checkHasEmptyHacks >>= checkTargetHasMoney defender >>= checkPlayerIsAttackingThemselves defender |> function - | Ok _ -> - let embed = Embeds.pickHack "Attack" attacker defender false + | Ok atkr -> + let embed = Embeds.pickHack "Attack" atkr defender false ctx.FollowUpAsync(embed) |> Async.AwaitTask |> Async.Ignore @@ -134,30 +149,32 @@ let attack (target : DiscordUser) (ctx : InteractionContext) = let handleAttack (event : ComponentInteractionCreateEventArgs) = Game.executePlayerEvent event (fun attacker -> async { let split = event.Id.Split("-") - let hack = enum(int split.[1]) + 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 + |> Player.removeExpiredActions false |> checkAlreadyHackedTarget defender.DiscordId - >>= (checkHackHasCooldown (int hack)) + >>= checkPlayerOwnsWeapon hackId + >>= checkItemHasCooldown hackId |> function - | Ok _ -> - runHackerBattle defender (Armory.getItem (int hack)) + | Ok atkr -> + runHackerBattle defender (Armory.getItem (int hackId)) |> function - | false -> successfulHack event attacker defender hack - | true -> failedHack event attacker defender hack + | 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.shields player |> Array.length > 0 then - let p = Player.removeExpiredActions player + 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 @@ -172,33 +189,25 @@ let handleDefense (event : ComponentInteractionCreateEventArgs) = let split = event.Id.Split("-") let shieldId = int split.[1] let shield = Armory.getItem shieldId - let updatedDefenses = player |> Player.removeExpiredActions |> Player.defenses - let alreadyUsedShield = updatedDefenses |> Array.exists (fun d -> d.ActionId = shieldId) - match alreadyUsedShield , updatedDefenses.Length < 2 with - | false , true -> - 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 - | _ , false -> - let timestamp = updatedDefenses |> Array.rev |> Array.head |> fun a -> a.Timestamp // This should be the next expiring timestamp - let cooldown = getTimeTillCooldownFinishes (TimeSpan.FromMinutes(int shield.Cooldown)) timestamp - do! sendFollowUpMessage event $"You are only allowed two shields at a time. Wait {cooldown} to add another shield" - do! DbService.updatePlayer <| { player with Actions = updatedDefenses } - | true , _ -> - let timestamp = updatedDefenses |> Array.find (fun d -> d.ActionId = int shieldId) |> fun a -> a.Timestamp - let cooldown = getTimeTillCooldownFinishes (TimeSpan.FromMinutes(int shield.Cooldown)) timestamp - do! sendFollowUpMessage event $"{shield.Name} shield is already in use. Wait {cooldown} to use this shield again" - do! DbService.updatePlayer <| { player with Actions = updatedDefenses } + 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) = diff --git a/Bot/Store.fs b/Bot/Store.fs index c345c1f..154595a 100644 --- a/Bot/Store.fs +++ b/Bot/Store.fs @@ -9,16 +9,6 @@ open Degenz open Degenz.Embeds open Degenz.Messaging -let handleResultWithResponse ctx fn (player : Result) = - match player with - | Ok p -> fn p - | Error e -> async { do! sendFollowUpMessageFromCtx ctx e } - -let handleResultWithResponseFromEvent event fn (player : Result) = - match player with - | Ok p -> fn p - | Error e -> async { do! sendFollowUpMessage event e } - let checkHasSufficientFunds (item : BattleItem) player = if player.Bank - item.Cost >= 0 then Ok player @@ -39,15 +29,37 @@ let checkHasItemsInArsenal itemType player = then Ok player else Error $"You currently have no {itemType}s in your arsenal to sell!" +let battleItemFormat (items : BattleItem array) = + match items with + | [||] -> "None" + | _ -> items |> Array.toList |> List.map (fun i -> i.Name) |> String.concat ", " + +let actionFormat (actions : Action array) = + match actions with + | [||] -> "None" + | _ -> + actions + |> Array.map (fun act -> + let item = Armory.getItem act.ActionId + match act.Type with + | Attack atk -> + let cooldown = getTimeText false Game.SameTargetAttackCooldown act.Timestamp + $"Hacked {atk.Target.Name} {cooldown} ago" + | Defense -> + let cooldown = getTimeText true (System.TimeSpan.FromMinutes(int item.Cooldown)) act.Timestamp + $"{item.Name} Shield active for {cooldown}") + |> String.concat "\n" + let statusFormat p = $"**Hacks:** {Player.hacks p |> battleItemFormat}\n -**Shields:** {Player.shields p |> battleItemFormat}\n +**Shields:** {Player.getShields p |> battleItemFormat}\n **Hack Attacks:**\n{Player.attacks p |> actionFormat}\n **Active Shields:**\n{Player.defenses p |> actionFormat}" +// TODO: There's a 1000 character limit for embeds, so you need to filter by N last actions let arsenal (ctx : InteractionContext) = Game.executePlayerInteraction ctx (fun player -> async { - let updatedPlayer = Player.removeExpiredActions player + let updatedPlayer = Player.removeExpiredActions false player let builder = DiscordFollowupMessageBuilder() let embed = DiscordEmbedBuilder() embed.AddField("Arsenal", statusFormat updatedPlayer) |> ignore diff --git a/Bot/Trainer.fs b/Bot/Trainer.fs index 9561a5f..f0bab31 100644 --- a/Bot/Trainer.fs +++ b/Bot/Trainer.fs @@ -1,7 +1,6 @@ module Degenz.Trainer open DSharpPlus -open System.Threading.Tasks open DSharpPlus.Entities open DSharpPlus.EventArgs open DSharpPlus.SlashCommands @@ -34,10 +33,10 @@ let sendInitialEmbed (client : DiscordClient) = let handleTrainerStep1 (event : ComponentInteractionCreateEventArgs) = Game.executePlayerEvent event (fun player -> async { let shieldMessage , weaponName = - if Player.shields player |> Array.isEmpty + if Player.getShields player |> Array.isEmpty then $"You do not have any Shields in your arsenal, take this {defaultShield.Name}, you can use it for now.\n\n" , defaultShield.Name else - let name = Player.shields player |> Array.tryHead |> Option.defaultValue defaultShield |> fun w -> w.Name + let name = Player.getShields player |> Array.tryHead |> Option.defaultValue defaultShield |> fun w -> w.Name $"Looks like you have `{name}` in your arsenal… 👀\n\n" , name let membr = event.User :?> DiscordMember @@ -56,7 +55,7 @@ let handleTrainerStep1 (event : ComponentInteractionCreateEventArgs) = let defend (ctx : InteractionContext) = Game.executePlayerInteraction ctx (fun player -> async { let playerWithShields = - match Player.shields player with + match Player.getShields player with | [||] -> { player with Arsenal = [| defaultShield |] } | _ -> player diff --git a/Shared/Shared.fs b/Shared/Shared.fs index 2711341..3576f33 100644 --- a/Shared/Shared.fs +++ b/Shared/Shared.fs @@ -10,6 +10,7 @@ open Newtonsoft.Json [] module ResultHelpers = let (>>=) x f = Result.bind f x + let () x f = Result.map f x [] module Types = @@ -94,32 +95,23 @@ module Messaging = Message : string } - let getTimeTillCooldownFinishes (timespan : TimeSpan) timestamp = - let remaining = timespan - (DateTime.UtcNow - timestamp) + let getTimeText isCooldown (timespan : TimeSpan) timestamp = + let span = + if isCooldown + then timespan - (DateTime.UtcNow - timestamp) + else (DateTime.UtcNow - timestamp) let plural amount = if amount = 1 then "" else "s" - let ``and`` = if remaining.Hours > 0 then "and " else "" - let hours = if remaining.Hours > 0 then $"{remaining.Hours} hour{plural remaining.Hours} {``and``}" else String.Empty - let totalMins = remaining.Minutes + let ``and`` = if span.Hours > 0 then "and " else "" + let hours = if span.Hours > 0 then $"{span.Hours} hour{plural span.Hours} {``and``}" else String.Empty + let totalMins = span.Minutes let minutes = if totalMins > 0 then $"{totalMins} minute{plural totalMins}" else "1 minute" $"{hours}{minutes}" - let battleItemFormat (items : BattleItem array) = - match items with - | [||] -> "None" - | _ -> items |> Array.toList |> List.map (fun i -> i.Name) |> String.concat ", " - - let actionFormat (actions : Action array) = - match actions with - | [||] -> "None" - | _ -> - actions - |> Array.map (fun act -> - let item = Armory.getItem act.ActionId - let cooldown = getTimeTillCooldownFinishes (System.TimeSpan.FromMinutes(int item.Cooldown)) act.Timestamp - match act.Type with - | Attack atk -> $"Hacked {atk.Target.Name} {cooldown} ago" - | Defense -> $"{item.Name} Shield active for {cooldown}") - |> String.concat "\n" + let getShortTimeText (timespan : TimeSpan) timestamp = + let remaining = timespan - (DateTime.UtcNow - timestamp) + let hours = if remaining.Hours > 0 then $"{remaining.Hours}h " else String.Empty + let minutesRemaining = if remaining.Hours = 0 then remaining.Minutes + 1 else remaining.Minutes + $"{hours}{minutesRemaining}min" let sendSimpleResponse (ctx: InteractionContext) msg = async { @@ -193,3 +185,13 @@ module Messaging = do! event.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, builder) |> Async.AwaitTask } + let handleResultWithResponse ctx fn (player : Result) = + match player with + | Ok p -> fn p + | Error e -> async { do! sendFollowUpMessageFromCtx ctx e } + + let handleResultWithResponseFromEvent event fn (player : Result) = + match player with + | Ok p -> fn p + | Error e -> async { do! sendFollowUpMessage event e } +