diff --git a/Bot/Bot.fs b/Bot/Bot.fs index 6982c29..3de342b 100644 --- a/Bot/Bot.fs +++ b/Bot/Bot.fs @@ -4,7 +4,6 @@ open System.Threading.Tasks open DSharpPlus open DSharpPlus.SlashCommands open Degenz -open Degenz.PlayerInteractions open Degenz.HackerBattle open Degenz.Store open Emzi0767.Utilities @@ -12,46 +11,41 @@ open Emzi0767.Utilities type EmptyGlobalCommandToAvoidFamousDuplicateSlashCommandsBug() = inherit ApplicationCommandModule () -let playerInteractionsConfig = DiscordConfiguration() +let guild = GuildEnvironment.guildId + let hackerBattleConfig = DiscordConfiguration() let storeConfig = DiscordConfiguration() //let slotMachineConfig = DiscordConfiguration() //hackerBattleConfig.MinimumLogLevel <- Microsoft.Extensions.Logging.LogLevel.Trace -//let configs = [| playerInteractionsConfig ; hackerBattleConfig ; storeConfig ; slotMachineConfig ; |] -let configs = [| playerInteractionsConfig ; hackerBattleConfig ; storeConfig |] +//let configs = [| hackerBattleConfig ; storeConfig ; slotMachineConfig ; |] +let configs = [ hackerBattleConfig ; storeConfig ] for conf in configs do conf.TokenType <- TokenType.Bot conf.Intents <- DiscordIntents.All -let guild = GuildEnvironment.guildId -playerInteractionsConfig.Token <- GuildEnvironment.tokenPlayerInteractions hackerBattleConfig.Token <- GuildEnvironment.tokenHackerBattle storeConfig.Token <- GuildEnvironment.tokenStore //slotMachineConfig.Token <- Environment.GetEnvironmentVariable("BOT_SLOT_MACHINE") -//let playerInteractionsBot = new DiscordClient(playerInteractionsConfig) let hackerBattleBot = new DiscordClient(hackerBattleConfig) let storeBot = new DiscordClient(storeConfig) //let slotMachineBot = new DiscordClient(slotMachineConfig) -//let clients = [| storeBot ; trainerBot ; hackerBattleBot ; playerInteractionsBot ; slotMachineBot |] -//let clients = [| hackerBattleBot ; storeBot ; playerInteractionsBot |] -let clients = [| hackerBattleBot ; storeBot |] +//let clients = [| hackerBattleBot ; storeBot ; slotMachineBot |] +let clients = [ hackerBattleBot ; storeBot ] -//let sc1 = playerInteractionsBot.UseSlashCommands() -let sc3 = hackerBattleBot.UseSlashCommands() -let sc4 = storeBot.UseSlashCommands() -//let sc5 = slotMachineBot.UseSlashCommands() +let sc1 = hackerBattleBot.UseSlashCommands() +let sc2 = storeBot.UseSlashCommands() +//let sc3 = slotMachineBot.UseSlashCommands() -//sc1.RegisterCommands(guild); -sc3.RegisterCommands(guild); -sc4.RegisterCommands(guild); -//sc5.RegisterCommands(guild); +sc1.RegisterCommands(guild); +sc2.RegisterCommands(guild); +//sc3.RegisterCommands(guild); hackerBattleBot.add_ComponentInteractionCreated(AsyncEventHandler(HackerBattle.handleButtonEvent)) -storeBot.add_ComponentInteractionCreated(AsyncEventHandler(Store.handleSellButtonEvents)) +storeBot.add_ComponentInteractionCreated(AsyncEventHandler(Store.handleStoreEvents)) let asdf (_ : DiscordClient) (event : DSharpPlus.EventArgs.InteractionCreateEventArgs) = async { @@ -74,11 +68,10 @@ let run (client : DiscordClient) = do! client.ConnectAsync () |> Async.AwaitTask } -Trainer.sendInitialEmbed hackerBattleBot +//Trainer.sendInitialEmbed hackerBattleBot clients -|> Array.map run -|> Array.toSeq +|> List.map run |> Async.Sequential |> Async.RunSynchronously |> ignore diff --git a/Bot/Bot.fsproj b/Bot/Bot.fsproj index c3c069f..0839f34 100644 --- a/Bot/Bot.fsproj +++ b/Bot/Bot.fsproj @@ -1,28 +1,28 @@  - - Exe - net6.0 - Linux - Degenz - - - - PreserveNewest - - - - - - - - - - - - - - - - + + Exe + net6.0 + Linux + Degenz + + + + PreserveNewest + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Bot/Embeds.fs b/Bot/Embeds.fs index cdac675..e31b83b 100644 --- a/Bot/Embeds.fs +++ b/Bot/Embeds.fs @@ -4,35 +4,22 @@ open System open DSharpPlus.EventArgs open Degenz.Types open DSharpPlus.Entities -open AsciiTableFormatter let hackGif = "https://s10.gifyu.com/images/Hacker-Degenz-V20ce8eb832734aa62-min.gif" let shieldGif = "https://s10.gifyu.com/images/Defense-Degenz-V2-min.gif" let getHackGif = function - | HackId.Virus -> "https://s10.gifyu.com/images/Attack-DegenZ-1.gif" - | HackId.RemoteAccess -> "https://s10.gifyu.com/images/Mind-Control-Degenz-V2-min.gif" - | HackId.Worm -> "https://s10.gifyu.com/images/WormBugAttack_Degenz-min.gif" + | HackId.Virus -> "https://s10.gifyu.com/images/Virus-icon.jpg" + | HackId.RemoteAccess -> "https://s10.gifyu.com/images/Mind-Control-Degenz-V2-min.jpg" + | HackId.Worm -> "https://s10.gifyu.com/images/WormBugAttack_Degenz-min.jpg" | _ -> hackGif let getShieldGif = function - | ShieldId.Firewall -> "https://s10.gifyu.com/images/Defense-GIF-1-Degenz-min.gif" - | ShieldId.Encryption -> "https://s10.gifyu.com/images/Encryption-Degenz-V2-1-min.gif" - | ShieldId.Cypher -> "https://s10.gifyu.com/images/Cypher-Smaller.gif" + | ShieldId.Firewall -> "https://s10.gifyu.com/images/Defense-GIF-1-Degenz-1.jpg" + | ShieldId.Encryption -> "https://s10.gifyu.com/images/Encryption-Degenz-V2-1-min.jpg" + | ShieldId.Cypher -> "https://s10.gifyu.com/images/Cypher-Smaller.jpg" | _ -> shieldGif - -let constructEmbed message = - let builder = DiscordEmbedBuilder() - builder.Color <- Optional(DiscordColor.Blurple) - builder.Description <- message - let author = DiscordEmbedBuilder.EmbedAuthor() - author.Name <- "Degenz Hacker Game" - author.Url <- "https://twitter.com/degenzgame" - author.IconUrl <- "https://pbs.twimg.com/profile_images/1473192843359309825/cqjm0VQ4_400x400.jpg" - builder.Author <- author - builder.Build() - let pickDefense actionId player = let buttons = Messaging.constructButtons actionId (string player.DiscordId) (Player.shields player) @@ -85,43 +72,73 @@ let responseCreatedShieldTrainer (shield : BattleItem) = DiscordFollowupMessageBuilder() .AddEmbed(DiscordEmbedBuilder().WithImageUrl(getShieldGif (enum(shield.Id)))) .AsEphemeral(true) - .WithContent($"Mounted a {shield.Name} defense for 6 hours") + .WithContent($"Mounted a {shield.Name} defense for {TimeSpan.FromMinutes(int shield.Cooldown).Hours} hours") let eventSuccessfulHack (event : ComponentInteractionCreateEventArgs) targetId prize = 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(hackGif) -// DiscordMessageBuilder() .WithContent($"{event.User.Username} successfully hacked <@{targetId}> for a total of {prize} GoodBoyTokenz") -[] -type Table = { - Name : string - Cost : string - Class : string -} +let getGoodAgainst = function + | BattleClass.Network -> ( ShieldId.Firewall , HackId.Virus ) + | BattleClass.Penetration -> ( ShieldId.Cypher , HackId.RemoteAccess ) + | BattleClass.Exploit -> ( ShieldId.Encryption , HackId.Worm ) -let storeListing store = - let embeds = +let getBuyItemsEmbed (itemType : ItemType) (store : BattleItem array) = + let embeds , buttons = store - |> Array.groupBy (fun (bi : BattleItem) -> bi.Type) - |> Array.map (fun ( itemType , items ) -> - let msg = - items - |> Array.map (fun item -> { Name = item.Name ; Cost = string item.Cost ; Class = string item.Class }) - |> Formatter.Format - |> sprintf "**%As**\n``` %s ```" itemType - DiscordEmbedBuilder() - .WithDescription(msg) - .Build()) + |> Array.filter (fun i -> i.Type = itemType) + |> Array.map (fun item -> + let embed = DiscordEmbedBuilder() + match item.Type with + | Hack -> + embed + .AddField($"Weak Against |", getGoodAgainst item.Class |> fst |> string , true) + .AddField("Cooldown |", $"{TimeSpan.FromMinutes(int item.Cooldown).Minutes} minutes", true) + .WithThumbnail(getHackGif (enum(item.Id))) + |> ignore + | Shield -> + embed + .AddField($"Strong Against |", getGoodAgainst item.Class |> snd |> string , true) + .AddField("Active For |", $"{TimeSpan.FromMinutes(int item.Cooldown).Hours} hours", true) + .WithThumbnail(getShieldGif (enum(item.Id))) + |> ignore + embed + .AddField("Cost 💰", $"{item.Cost} $GBT", true) + .WithColor(Game.getClassEmbedColor item.Class) + .WithTitle($"{item.Name}") + |> ignore + let button = DiscordButtonComponent(Game.getClassButtonColor item.Class, $"Buy-{item.Id}", $"Buy {item.Name}") + embed.Build() , button :> DiscordComponent) + |> Array.unzip - DiscordInteractionResponseBuilder() + DiscordFollowupMessageBuilder() .AddEmbeds(embeds) + .AddComponents(buttons) + .AsEphemeral(true) + +let getSellItemsEmbed (itemType : ItemType) (player : PlayerData) = + let embeds , buttons = + player.Arsenal + |> Array.filter (fun i -> i.Type = itemType) + |> Array.map (fun item -> + let embed = DiscordEmbedBuilder() + match item.Type with + | Hack -> embed.WithThumbnail(getHackGif (enum(item.Id))) |> ignore + | Shield -> embed.WithThumbnail(getShieldGif (enum(item.Id))) |> ignore + embed + .AddField("Sell For 💰", $"{item.Cost} $GBT", true) + .WithColor(Game.getClassEmbedColor item.Class) + .WithTitle($"{item.Name}") + |> ignore + let button = DiscordButtonComponent(Game.getClassButtonColor item.Class, $"Sell-{item.Id}", $"Sell {item.Name}") + embed.Build() , button :> DiscordComponent) + |> Array.unzip + + DiscordFollowupMessageBuilder() + .AddEmbeds(embeds) + .AddComponents(buttons) .AsEphemeral(true) diff --git a/Bot/Game.fs b/Bot/Game.fs index c4889b4..9c71e44 100644 --- a/Bot/Game.fs +++ b/Bot/Game.fs @@ -1,43 +1,78 @@ -module Degenz.Game +namespace Degenz -open System open System.Threading.Tasks open DSharpPlus open DSharpPlus.Entities open DSharpPlus.EventArgs open DSharpPlus.SlashCommands open Degenz.DbService -open Microsoft.VisualBasic -let HackPrize = 10 -let ShieldPrize = 5 +module Game = + let HackPrize = 10 + let ShieldPrize = 5 -let executePlayerInteraction (ctx : InteractionContext) (dispatch : PlayerData -> Async) = - async { - let builder = DiscordInteractionResponseBuilder() - builder.IsEphemeral <- true - builder.Content <- "Content" - do! ctx.CreateResponseAsync(InteractionResponseType.DeferredChannelMessageWithSource, builder) - |> Async.AwaitTask - let! playerResult = tryFindPlayer ctx.Member.Id - match playerResult with - | Some player -> do! dispatch player - | None -> do! Messaging.sendSimpleResponse ctx "You are currently not a hacker, first use the /redpill command to become one" - } |> Async.StartAsTask - :> Task + let SameTargetAttackCooldown = System.TimeSpan.FromHours(6) -// TODO: Create an abstraction for these two helper functions -let executePlayerEvent (event : ComponentInteractionCreateEventArgs) (dispatch : PlayerData -> Async) = - async { - let builder = DiscordInteractionResponseBuilder() - builder.IsEphemeral <- true - builder.Content <- "Content" - do! event.Interaction.CreateResponseAsync(InteractionResponseType.DeferredChannelMessageWithSource, builder) - |> Async.AwaitTask - let! playerResult = tryFindPlayer event.User.Id - match playerResult with - | Some player -> do! dispatch player - | None -> do! Messaging.sendInteractionEvent event "You are currently not a hacker, first use the /redpill command to become one" - } |> Async.StartAsTask - :> Task + let getClassButtonColor = function + | Network -> ButtonStyle.Danger + | Exploit -> ButtonStyle.Success + | Penetration -> ButtonStyle.Primary + + let getClassEmbedColor = function + | Network -> DiscordColor.Red + | Penetration -> DiscordColor.Blurple + | Exploit -> DiscordColor.Green + + let executePlayerInteraction (ctx : InteractionContext) (dispatch : PlayerData -> Async) = + async { + let builder = DiscordInteractionResponseBuilder() + builder.IsEphemeral <- true + builder.Content <- "Content" + do! ctx.CreateResponseAsync(InteractionResponseType.DeferredChannelMessageWithSource, builder) + |> Async.AwaitTask + let! playerResult = tryFindPlayer ctx.Member.Id + match playerResult with + | Some player -> do! dispatch player + | None -> do! Messaging.sendSimpleResponse ctx "You are currently not a hacker, first use the /redpill command to become one" + } |> Async.StartAsTask + :> Task + + // TODO: Create an abstraction for these two helper functions + let executePlayerEvent (event : ComponentInteractionCreateEventArgs) (dispatch : PlayerData -> Async) = + async { + let builder = DiscordInteractionResponseBuilder() + builder.IsEphemeral <- true + builder.Content <- "Content" + do! event.Interaction.CreateResponseAsync(InteractionResponseType.DeferredChannelMessageWithSource, builder) + |> Async.AwaitTask + let! playerResult = tryFindPlayer event.User.Id + match playerResult with + | Some player -> do! dispatch player + | None -> do! Messaging.sendInteractionEvent event "You are currently not a hacker, first use the /redpill command to become one" + } |> Async.StartAsTask + :> 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 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 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)) + { player with Actions = actions } + + let modifyBank (player : PlayerData) amount = { player with Bank = max (player.Bank + amount) 0 } + + let getAttacksFlat actions = actions |> Array.choose (fun act -> match act.Type with Attack ar -> Some (act,ar.Target,ar.Result) | Defense -> None) diff --git a/Bot/HackerBattle.fs b/Bot/HackerBattle.fs index 87c9b7a..ca10e45 100644 --- a/Bot/HackerBattle.fs +++ b/Bot/HackerBattle.fs @@ -9,24 +9,22 @@ open DSharpPlus.SlashCommands open Degenz open Degenz.Messaging -let (>>=) x f = Result.bind f x - -let checkIfPlayerIsAttackingThemselves defender attacker = +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 checkForExistingTarget defenderId attacker = +let checkAlreadyHackedTarget defenderId attacker = attacker.Actions |> Player.getAttacksFlat |> Array.tryFind (fun (_,t,_) -> t.Id = defenderId) |> function | Some ( atk , target , _ ) -> - let cooldown = getTimeTillCooldownFinishes Player.SameTargetAttackCooldown atk.Timestamp - Error $"You can only hack the same target once every {Player.SameTargetAttackCooldown.Hours} hours, wait {cooldown} to attempt another hack on {target.Name}." + let cooldown = getTimeTillCooldownFinishes 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 checkIfHackHasCooldown hackId attacker = +let checkHackHasCooldown hackId attacker = let mostRecentHackAttack = attacker.Actions |> Array.tryFind (fun a -> a.ActionId = hackId) @@ -41,12 +39,12 @@ let checkIfHackHasCooldown hackId attacker = let item = Armory.battleItems |> Array.find (fun i -> i.Id = hackId) Error $"{item.Name} is currently on cooldown, wait {cooldown} to use it again." -let checkIfHasEmptyHacks attacker = +let checkHasEmptyHacks attacker = match Player.hacks 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 checkIfTargetHasMoney (target : PlayerData) attacker = +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 @@ -116,24 +114,17 @@ let attack (target : DiscordUser) (ctx : InteractionContext) = | Some defender -> do! attacker |> Player.removeExpiredActions - |> checkForExistingTarget defender.DiscordId - >>= checkIfHasEmptyHacks - >>= checkIfTargetHasMoney defender - >>= checkIfPlayerIsAttackingThemselves defender + |> checkAlreadyHackedTarget defender.DiscordId + >>= checkHasEmptyHacks + >>= checkTargetHasMoney defender + >>= checkPlayerIsAttackingThemselves defender |> function | Ok _ -> let embed = Embeds.pickHack "Attack" attacker defender ctx.FollowUpAsync(embed) |> Async.AwaitTask |> Async.Ignore - | Error msg -> - let builder = - DiscordFollowupMessageBuilder() - .WithContent(msg) - .AsEphemeral(true) - ctx.FollowUpAsync(builder) - |> Async.AwaitTask - |> Async.Ignore + | Error msg -> sendFollowUpMessageFromCtx ctx msg | None -> do! sendFollowUpMessageFromCtx ctx "Your target is not connected to the network, they must join first by using the /redpill command" }) @@ -148,8 +139,8 @@ let handleAttack (event : ComponentInteractionCreateEventArgs) = | Some defender , true , true -> do! attacker |> Player.removeExpiredActions - |> checkForExistingTarget defender.DiscordId - >>= (checkIfHackHasCooldown (int hack)) + |> checkAlreadyHackedTarget defender.DiscordId + >>= (checkHackHasCooldown (int hack)) |> function | Ok _ -> runHackerBattle defender (Armory.getItem (int hack)) @@ -175,17 +166,18 @@ let defend (ctx : InteractionContext) = let handleDefense (event : ComponentInteractionCreateEventArgs) = Game.executePlayerEvent event (fun player -> async { let split = event.Id.Split("-") - let shieldId = enum(int split.[1]) + 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 = int shieldId) + let alreadyUsedShield = updatedDefenses |> Array.exists (fun d -> d.ActionId = shieldId) match alreadyUsedShield , updatedDefenses.Length < 2 with | false , true -> - let embed = Embeds.responseCreatedShield (Armory.getItem (int shieldId)) + let embed = Embeds.responseCreatedShield (Armory.getItem shieldId) do! event.Interaction.CreateFollowupMessageAsync(embed) |> Async.AwaitTask |> Async.Ignore - let defense = { ActionId = int shieldId ; Type = Defense ; Timestamp = DateTime.UtcNow } + 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 @@ -194,24 +186,14 @@ let handleDefense (event : ComponentInteractionCreateEventArgs) = |> Async.AwaitTask |> Async.Ignore | _ , false -> - let builder = DiscordFollowupMessageBuilder() - builder.IsEphemeral <- true let timestamp = updatedDefenses |> Array.rev |> Array.head |> fun a -> a.Timestamp // This should be the next expiring timestamp - let cooldown = getTimeTillCooldownFinishes (TimeSpan.FromHours(6)) timestamp - builder.Content <- $"You are only allowed two shields at a time. Wait {cooldown} minutes to add another shield" - do! event.Interaction.CreateFollowupMessageAsync(builder) - |> Async.AwaitTask - |> Async.Ignore + let cooldown = getTimeTillCooldownFinishes (TimeSpan.FromMinutes(int shield.Cooldown)) timestamp + do! sendFollowUpMessage event $"You are only allowed two shields at a time. Wait {cooldown} minutes to add another shield" do! DbService.updatePlayer <| { player with Actions = updatedDefenses } | true , _ -> - let builder = DiscordFollowupMessageBuilder() - builder.IsEphemeral <- true let timestamp = updatedDefenses |> Array.find (fun d -> d.ActionId = int shieldId) |> fun a -> a.Timestamp - let cooldown = getTimeTillCooldownFinishes (TimeSpan.FromHours(6)) timestamp - builder.Content <- $"{shieldId} shield is already in use. Wait {cooldown} minutes to use this shield again" - do! event.Interaction.CreateFollowupMessageAsync(builder) - |> Async.AwaitTask - |> Async.Ignore + let cooldown = getTimeTillCooldownFinishes (TimeSpan.FromMinutes(int shield.Cooldown)) timestamp + do! sendFollowUpMessage event $"{shieldId} shield is already in use. Wait {cooldown} minutes to use this shield again" do! DbService.updatePlayer <| { player with Actions = updatedDefenses } }) diff --git a/Bot/Items.json b/Bot/Items.json index c5ed359..bb67ab4 100644 --- a/Bot/Items.json +++ b/Bot/Items.json @@ -8,7 +8,7 @@ "Class": { "Case": "Network" }, - "Cost": 100, + "Cost": 50, "Power": 50, "Cooldown": 2 }, @@ -21,7 +21,7 @@ "Class": { "Case": "Penetration" }, - "Cost": 100, + "Cost": 50, "Power": 50, "Cooldown": 2 }, @@ -34,7 +34,7 @@ "Class": { "Case": "Exploit" }, - "Cost": 100, + "Cost": 50, "Power": 50, "Cooldown": 2 }, @@ -47,20 +47,7 @@ "Class": { "Case": "Network" }, - "Cost": 100, - "Power": 50, - "Cooldown": 600 - }, - { - "Id": 7, - "Name": "Encryption", - "Type": { - "Case": "Shield" - }, - "Class": { - "Case": "Exploit" - }, - "Cost": 100, + "Cost": 50, "Power": 50, "Cooldown": 600 }, @@ -73,7 +60,20 @@ "Class": { "Case": "Penetration" }, - "Cost": 100, + "Cost": 50, + "Power": 50, + "Cooldown": 600 + }, + { + "Id": 7, + "Name": "Encryption", + "Type": { + "Case": "Shield" + }, + "Class": { + "Case": "Exploit" + }, + "Cost": 50, "Power": 50, "Cooldown": 600 } diff --git a/Bot/Store.fs b/Bot/Store.fs index 108e644..88aae98 100644 --- a/Bot/Store.fs +++ b/Bot/Store.fs @@ -9,90 +9,48 @@ open Degenz open Degenz.Embeds open Degenz.Messaging -let viewStore (ctx : InteractionContext) = - async { - do! ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, Embeds.storeListing Armory.battleItems) - |> Async.AwaitTask - } |> Async.StartAsTask - :> Task +let handleResultWithResponse ctx fn (player : Result) = + match player with + | Ok p -> fn p + | Error e -> async { do! sendFollowUpMessageFromCtx ctx e } -let buyItem (ctx : InteractionContext) itemId = - Game.executePlayerInteraction ctx (fun player -> async { - let item = Armory.getItem itemId - let newBalance = player.Bank - item.Cost - if newBalance >= 0 then - let playerHasItem = player.Arsenal |> Array.exists (fun w -> item.Id = w.Id) - if not playerHasItem then - let p = { player with Bank = newBalance ; Arsenal = Array.append [| item |] player.Arsenal } - do! DbService.updatePlayer p - do! sendFollowUpMessageFromCtx ctx $"Successfully purchased {item.Name}! You now have {newBalance} 💰$GBT remaining" - else - do! sendFollowUpMessageFromCtx ctx $"You already own this item!" - else - do! sendFollowUpMessageFromCtx ctx $"You do not have sufficient funds to buy this item! Current balance: {player.Bank} GBT" - }) +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 + else Error $"You do not have sufficient funds to buy this item! Current balance: {player.Bank} GBT" -let sell (ctx : InteractionContext) = - Game.executePlayerInteraction ctx (fun player -> async { - let hasInventoryToSell = Array.length player.Arsenal > 0 - if hasInventoryToSell then - let builder = DiscordFollowupMessageBuilder() - builder.AddEmbed (constructEmbed "Pick the item you wish to sell.") |> ignore +let checkAlreadyOwnsItem (item : BattleItem) player = + if player.Arsenal |> Array.exists (fun w -> item.Id = w.Id) + then Error $"You already own {item.Name}!" + else Ok player - Array.chunkBySize 5 player.Arsenal - |> Array.iter - (fun wps -> - wps - |> Array.map (fun w -> DiscordButtonComponent(ButtonStyle.Primary, $"{w.Type}-{w.Id}", $"{w.Name}")) - |> Seq.cast - |> builder.AddComponents - |> ignore) - builder.AsEphemeral true |> ignore +let checkSoldItemAlready (item : BattleItem) player = + if player.Arsenal |> Array.exists (fun w -> item.Id = w.Id) + then Ok player + else Error $"{item.Name} not found in your arsenal! Looks like you sold it already." - do! ctx.FollowUpAsync(builder) - |> Async.AwaitTask - |> Async.Ignore - else - do! sendFollowUpMessageFromCtx ctx "You currently have no inventory to sell" - }) +let checkHasItemsInArsenal itemType player = + if player.Arsenal |> Array.filter (fun i -> i.Type = itemType ) |> Array.length > 0 + then Ok player + else Error $"You currently have no {itemType}s in your arsenal to sell!" -let handleBuyItem (ctx : InteractionContext) itemId = - Game.executePlayerInteraction ctx (fun player -> async { - let item = Armory.battleItems |> Array.find (fun w -> w.Id = itemId) - let newBalance = player.Bank - item.Cost - if newBalance >= 0 then - let playerHasItem = player.Arsenal |> Array.exists (fun w -> item.Id = w.Id) - if not playerHasItem then - let p = { player with Bank = newBalance ; Arsenal = Array.append [| item |] player.Arsenal } - do! DbService.updatePlayer p - do! sendFollowUpMessageFromCtx ctx $"Successfully purchased {item.Name}! You now have {newBalance} 💰$GBT remaining" - else - do! sendFollowUpMessageFromCtx ctx $"You already own this item!" - else - do! sendFollowUpMessageFromCtx ctx $"You do not have sufficient funds to buy this item! Current balance: {player.Bank} GBT" - }) +let statusFormat p = + $"**Hacks:** {Player.hacks p |> battleItemFormat}\n +**Shields:** {Player.shields p |> battleItemFormat}\n +**Hack Attacks:**\n{Player.attacks p |> actionFormat}\n +**Active Shields:**\n{Player.defenses p |> actionFormat}" -let handleSellButtonEvents (_ : DiscordClient) (event : ComponentInteractionCreateEventArgs) = - Game.executePlayerEvent event (fun player -> async { - let itemId = int <| event.Id.Split("-").[1] - let item = Armory.getItem itemId - let updatedPlayer = { player with Bank = player.Bank + item.Cost ; Arsenal = player.Arsenal |> Array.filter (fun w -> w.Id <> itemId)} - do! DbService.updatePlayer updatedPlayer - let builder = DiscordFollowupMessageBuilder() - builder.IsEphemeral <- true - builder.Content <- $"Sold {item.Type} {item.Name} for {item.Cost}! Current Balance: {updatedPlayer.Bank}" - do! event.Interaction.CreateFollowupMessageAsync(builder) - |> Async.AwaitTask - |> Async.Ignore - }) - -let status (ctx : InteractionContext) = +let arsenal (ctx : InteractionContext) = Game.executePlayerInteraction ctx (fun player -> async { let updatedPlayer = Player.removeExpiredActions player let builder = DiscordFollowupMessageBuilder() let embed = DiscordEmbedBuilder() - embed.AddField("Arsenal", Messaging.statusFormat updatedPlayer) |> ignore + embed.AddField("Arsenal", statusFormat updatedPlayer) |> ignore builder.AddEmbed(embed) |> ignore builder.IsEphemeral <- true do! ctx.FollowUpAsync(builder) @@ -101,23 +59,83 @@ let status (ctx : InteractionContext) = do! DbService.updatePlayer updatedPlayer }) +let buy (ctx : InteractionContext) itemType = + Game.executePlayerInteraction ctx (fun player -> async { + let itemStore = Embeds.getBuyItemsEmbed itemType Armory.battleItems + do! ctx.Interaction.CreateFollowupMessageAsync(itemStore) + |> Async.AwaitTask + |> Async.Ignore + }) + +// TODO: Remove active shield when selling +let sell (ctx : InteractionContext) itemType = + Game.executePlayerInteraction ctx (fun player -> async { + match checkHasItemsInArsenal itemType player with + | Ok _ -> + let itemStore = Embeds.getSellItemsEmbed itemType player + + do! ctx.FollowUpAsync(itemStore) + |> Async.AwaitTask + |> Async.Ignore + | Error e -> do! sendFollowUpMessageFromCtx ctx e + }) + +// TODO: When you buy a shield, prompt the user to activate it +let handleBuyItem (event : ComponentInteractionCreateEventArgs) itemId = + Game.executePlayerEvent event (fun player -> async { + let item = Armory.battleItems |> Array.find (fun w -> w.Id = itemId) + do! player + |> checkHasSufficientFunds item + >>= checkAlreadyOwnsItem item + |> handleResultWithResponseFromEvent event (fun player -> async { + let newBalance = player.Bank - item.Cost + let p = { player with Bank = newBalance ; Arsenal = Array.append [| item |] player.Arsenal } + do! DbService.updatePlayer p + do! sendFollowUpMessage event $"Successfully purchased {item.Name}! You now have {newBalance} 💰$GBT remaining" + }) + }) + +let handleSell (event : ComponentInteractionCreateEventArgs) itemId = + Game.executePlayerEvent event (fun player -> async { + let item = Armory.getItem itemId + do! player + |> checkSoldItemAlready item + |> handleResultWithResponseFromEvent event (fun player -> async { + let updatedPlayer = { player with Bank = player.Bank + item.Cost ; Arsenal = player.Arsenal |> Array.filter (fun w -> w.Id <> itemId) } + do! DbService.updatePlayer updatedPlayer + do! sendFollowUpMessage event $"Sold {item.Type} {item.Name} for {item.Cost}! Current Balance: {updatedPlayer.Bank}" + }) + }) + +let handleStoreEvents (_ : DiscordClient) (event : ComponentInteractionCreateEventArgs) = + let itemId = int <| event.Id.Split("-").[1] + match event.Id with + | id when id.StartsWith("Buy") -> handleBuyItem event itemId + | id when id.StartsWith("Sell") -> handleSell event itemId + | _ -> + task { + let builder = DiscordInteractionResponseBuilder() + builder.IsEphemeral <- true + builder.Content <- $"Incorrect Action identifier {event.Id}" + do! event.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, builder) + |> Async.AwaitTask + } + type Store() = inherit ApplicationCommandModule () - [] - member _.Armory (ctx : InteractionContext) = viewStore ctx - [] - member this.Arsenal (ctx : InteractionContext) = status ctx + member this.Arsenal (ctx : InteractionContext) = arsenal ctx [] - member _.BuyHack (ctx : InteractionContext, [] hackId : HackId) = - buyItem ctx (int hackId) + member _.BuyHack (ctx : InteractionContext) = buy ctx ItemType.Hack [] - member this.BuyShield (ctx : InteractionContext, [] shieldId : ShieldId) = - buyItem ctx (int shieldId) + member this.BuyShield (ctx : InteractionContext) = buy ctx ItemType.Shield - [] - member this.SellItem (ctx : InteractionContext) = sell ctx + [] + member this.SellHack (ctx : InteractionContext) = sell ctx ItemType.Hack + + [] + member this.SellShield (ctx : InteractionContext) = sell ctx ItemType.Shield diff --git a/Shared/Shared.fs b/Shared/Shared.fs index bfd3d8a..f6febd4 100644 --- a/Shared/Shared.fs +++ b/Shared/Shared.fs @@ -7,6 +7,10 @@ open DSharpPlus.EventArgs open DSharpPlus.SlashCommands open Newtonsoft.Json +[] +module ResultHelpers = + let (>>=) x f = Result.bind f x + [] module Types = @@ -83,32 +87,6 @@ module Armory = let getItem itemId = battleItems |> Array.find (fun w -> w.Id = itemId) -module Player = - let SameTargetAttackCooldown = TimeSpan.FromHours(2) - let hacks player = player.Arsenal |> Array.filter (fun bi -> bi.Type = Hack) - let shields player = player.Arsenal |> Array.filter (fun bi -> bi.Type = Shield) - 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 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 _ -> DateTime.UtcNow - act.Timestamp < SameTargetAttackCooldown - | Defense -> - let item = Armory.getItem act.ActionId - DateTime.UtcNow - act.Timestamp < TimeSpan.FromMinutes(int item.Cooldown)) - { player with Actions = actions } - - let modifyBank player amount = { player with Bank = max (player.Bank + amount) 0 } - - let getAttacksFlat actions = actions |> Array.choose (fun act -> match act.Type with Attack ar -> Some (act,ar.Target,ar.Result) | Defense -> None) - - module Messaging = type InteractiveMessage = { ButtonId : string @@ -140,16 +118,10 @@ module Messaging = | Attack atk -> $"Hacked {atk.Target.Name} at {act.Timestamp.ToShortTimeString()}" | Defense -> let item = Armory.getItem act.ActionId - let cooldown = getTimeTillCooldownFinishes (TimeSpan.FromMinutes(int item.Cooldown)) act.Timestamp + let cooldown = getTimeTillCooldownFinishes (System.TimeSpan.FromMinutes(int item.Cooldown)) act.Timestamp $"{item.Name} active for {cooldown}") |> String.concat "\n" - let statusFormat p = - $"**Hacks:** {Player.hacks p |> battleItemFormat}\n -**Shields:** {Player.shields p |> battleItemFormat}\n -**Hack Attacks:**\n{Player.attacks p |> actionFormat}\n -**Active Shields:** {Player.defenses p |> actionFormat}" - let constructButtons (actionType: string) (playerInfo: string) (weapons: BattleItem array) = weapons |> Array.map (fun w -> DiscordButtonComponent(ButtonStyle.Success, $"{actionType}-{w.Id}-{playerInfo}", $"{w.Name}")) @@ -226,13 +198,3 @@ module Messaging = do! event.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, builder) |> Async.AwaitTask } - let sendInteractionEventWithButton (event : ComponentInteractionCreateEventArgs) buttonId msg = - async { - let builder = DiscordInteractionResponseBuilder() - let button = DiscordButtonComponent(ButtonStyle.Success, buttonId, "Got it") :> DiscordComponent - builder.AddComponents [| button |] |> ignore - builder.IsEphemeral <- true - builder.Content <- msg - do! event.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, builder) |> Async.AwaitTask - } -