From 785660558d3234386676766c697c8c42b7c1186d Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 11 Feb 2022 15:51:45 +0700 Subject: [PATCH] New trainer bot flow that gifts weapons or money, plus achievement --- Bot/Bot.fs | 50 ++++++++------- Bot/Bot.fsproj | 24 ++++---- Bot/Embeds.fs | 53 +++++++++++----- Bot/Game.fs | 40 +++++++++++- Bot/GuildEnvironment.fs | 8 ++- Bot/HackerBattle.fs | 24 +++++++- Bot/Store.fs | 48 --------------- Bot/Trainer.fs | 132 +++++++++++++++++++++++++++++++--------- DbService/DbService.fs | 26 +++++++- Shared/Shared.fs | 5 +- 10 files changed, 272 insertions(+), 138 deletions(-) diff --git a/Bot/Bot.fs b/Bot/Bot.fs index d46fc20..46b1190 100644 --- a/Bot/Bot.fs +++ b/Bot/Bot.fs @@ -66,30 +66,38 @@ let asdf (_ : DiscordClient) (event : DSharpPlus.EventArgs.InteractionCreateEven if guild <> 922419263275425832uL then Trainer.sendInitialEmbed hackerBattleBot -let run (client : DiscordClient) = - async { - do! client.ConnectAsync () |> Async.AwaitTask - } +hackerBattleBot.ConnectAsync() |> Async.AwaitTask |> Async.RunSynchronously +GuildEnvironment.botUserHackerBattle <- Some hackerBattleBot.CurrentUser +storeBot.ConnectAsync() |> Async.AwaitTask |> Async.RunSynchronously +GuildEnvironment.botUserArmory <- Some storeBot.CurrentUser -let clients = - if guild = 922419263275425832uL then - let interactionsConfig = DiscordConfiguration() - interactionsConfig.TokenType <- TokenType.Bot - interactionsConfig.Intents <- DiscordIntents.All - interactionsConfig.Token <- GuildEnvironment.tokenPlayerInteractions - let interactionsBot = new DiscordClient(interactionsConfig) - let commands = interactionsBot.UseSlashCommands() - commands.RegisterCommands(guild) +//async { +// let! user = hackerBattleBot.GetUserAsync(GuildEnvironment.botIdHackerBattle) |> Async.AwaitTask +// if user <> null then +// GuildEnvironment.botUserHackerBattle <- Some user +// return () +//} |> Async.RunSynchronously - [ hackerBattleBot ; storeBot ; interactionsBot ] - else - [ hackerBattleBot ; storeBot ] +//async { +// let! user = storeBot.GetUserAsync(GuildEnvironment.botIdHackerBattle) |> Async.AwaitTask +// if user <> null then +// GuildEnvironment.botUserHackerBattle <- Some user +// return () +//} |> Async.RunSynchronously + +if guild = 922419263275425832uL then + let interactionsConfig = DiscordConfiguration() + interactionsConfig.TokenType <- TokenType.Bot + interactionsConfig.Intents <- DiscordIntents.All + interactionsConfig.Token <- GuildEnvironment.tokenPlayerInteractions + + let interactionsBot = new DiscordClient(interactionsConfig) + + let commands = interactionsBot.UseSlashCommands() + commands.RegisterCommands(guild) + + interactionsBot.ConnectAsync() |> Async.AwaitTask |> Async.RunSynchronously -clients -|> List.map run -|> Async.Sequential -|> Async.RunSynchronously -|> ignore Task.Delay(-1) |> Async.AwaitTask diff --git a/Bot/Bot.fsproj b/Bot/Bot.fsproj index 0839f34..ea7d573 100644 --- a/Bot/Bot.fsproj +++ b/Bot/Bot.fsproj @@ -10,19 +10,19 @@ PreserveNewest - - - - - - - - - - + + + + + + + + + + - + - + \ No newline at end of file diff --git a/Bot/Embeds.fs b/Bot/Embeds.fs index 1315cfb..b0efa86 100644 --- a/Bot/Embeds.fs +++ b/Bot/Embeds.fs @@ -76,36 +76,30 @@ let pickHack actionId attacker defender isTrainer = .AddEmbed(embed.Build()) .AsEphemeral true -let responseSuccessfulHack (targetName : string) (hack : BattleItem) = +let responseSuccessfulHack earnedMoney (targetId : uint64) (hack : BattleItem) = let embed = DiscordEmbedBuilder() embed.ImageUrl <- getHackGif (enum(hack.Id)) + embed.Title <- "Hack Attack" + embed.Description <- $"You successfully hacked <@{targetId}> using {hack.Name}" + + (if earnedMoney then ", and stole 💰$GBT {Game.HackPrize} from them!" else "!") DiscordFollowupMessageBuilder() - .WithContent($"You successfully hacked {targetName} using {hack.Name}, and stole 💰$GBT {Game.HackPrize} from them!") .AddEmbed(embed.Build()) .AsEphemeral(true) let responseCreatedShield (shield : BattleItem) = - DiscordFollowupMessageBuilder() - .AddEmbed(DiscordEmbedBuilder().WithImageUrl(getShieldGif (enum(shield.Id)))) - .AsEphemeral(true) - .WithContent($"Mounted {shield.Name} shield for {TimeSpan.FromMinutes(int shield.Cooldown).Hours} hours") + let embed = DiscordEmbedBuilder().WithImageUrl(getShieldGif (enum(shield.Id))) + embed.Title <- "Mounted Shield" + embed.Description <- $"Mounted {shield.Name} shield for {TimeSpan.FromMinutes(int shield.Cooldown).Hours} hours" -let responseCreatedShieldTrainer (shield : BattleItem) = DiscordFollowupMessageBuilder() - .AddEmbed(DiscordEmbedBuilder().WithImageUrl(getShieldGif (enum(shield.Id)))) + .AddEmbed(embed) .AsEphemeral(true) - .WithContent($"Mounted a {shield.Name} defense for {TimeSpan.FromMinutes(int shield.Cooldown).Hours} hours") let eventSuccessfulHack (event : ComponentInteractionCreateEventArgs) target prize = DiscordMessageBuilder() .WithContent($"{event.User.Username} successfully hacked <@{target.DiscordId}> for a total of {prize} GoodBoyTokenz") -let getGoodAgainst = function - | BattleClass.Network -> ( ShieldId.Firewall , HackId.Virus ) - | BattleClass.Penetration -> ( ShieldId.Cypher , HackId.RemoteAccess ) - | BattleClass.Exploit -> ( ShieldId.Encryption , HackId.Worm ) - let getBuyItemsEmbed (player : PlayerData) (itemType : ItemType) (store : BattleItem array) = let embeds , buttons = store @@ -115,13 +109,13 @@ let getBuyItemsEmbed (player : PlayerData) (itemType : ItemType) (store : Battle match item.Type with | Hack -> embed - .AddField($"Weak Against |", getGoodAgainst item.Class |> fst |> string , true) + .AddField($"Weak Against |", Game.getGoodAgainst item.Class |> fst |> string , true) .AddField("Cooldown |", $"{TimeSpan.FromMinutes(int item.Cooldown).Minutes} minutes", true) .WithThumbnail(getHackIcon (enum(item.Id))) |> ignore | Shield -> embed - .AddField($"Strong Against |", getGoodAgainst item.Class |> snd |> string , true) + .AddField($"Strong Against |", Game.getGoodAgainst item.Class |> snd |> string , true) .AddField("Active For |", $"{TimeSpan.FromMinutes(int item.Cooldown).Hours} hours", true) .WithThumbnail(getShieldIcon (enum(item.Id))) |> ignore @@ -164,3 +158,30 @@ let getSellItemsEmbed (itemType : ItemType) (player : PlayerData) = .AddEmbeds(embeds) .AddComponents(buttons) .AsEphemeral(true) + +let getArsenalEmbed (player : PlayerData) = + DiscordFollowupMessageBuilder() + .AsEphemeral(true) + .AddEmbed( + DiscordEmbedBuilder() + .AddField( "Arsenal", Arsenal.statusFormat player )) + +let getAchievementEmbed (player : PlayerData) description achievement = + let embed = DiscordEmbedBuilder() + + GuildEnvironment.botUserHackerBattle + |> Option.iter (fun bot -> + embed.Author <- DiscordEmbedBuilder.EmbedAuthor() + embed.Author.Name <- bot.Username + embed.Author.IconUrl <- bot.AvatarUrl) + + DiscordFollowupMessageBuilder() + .AddEmbed( + // TODO: We can add a Reward field but we'd need to keep track of what the player was awarded + embed.WithTitle("Achievement Unlocked!") + .WithDescription(description) + .WithColor(DiscordColor.Gold) + .AddField("Achievement", achievement) + // TODO: Once we add another achievement, fix this + .WithImageUrl("https://s10.gifyu.com/images/MasterTraining_Degenz.gif")) + .AsEphemeral(true) \ No newline at end of file diff --git a/Bot/Game.fs b/Bot/Game.fs index 2ae2c29..158f2aa 100644 --- a/Bot/Game.fs +++ b/Bot/Game.fs @@ -23,6 +23,11 @@ module Game = | Penetration -> DiscordColor.Blurple | Exploit -> DiscordColor.Green + let getGoodAgainst = function + | BattleClass.Network -> ( ShieldId.Firewall , HackId.Virus ) + | BattleClass.Penetration -> ( ShieldId.Cypher , HackId.RemoteAccess ) + | BattleClass.Exploit -> ( ShieldId.Encryption , HackId.Worm ) + let executePlayerInteraction (ctx : InteractionContext) (dispatch : PlayerData -> Async) = async { let builder = DiscordInteractionResponseBuilder() @@ -54,9 +59,9 @@ module Game = module Player = let getItems itemType (player : PlayerData) = player.Arsenal |> Array.filter (fun i -> i.Type = itemType) - let hacks (player : PlayerData) = getItems ItemType.Hack player + let getHacks (player : PlayerData) = getItems ItemType.Hack player let getShields (player : PlayerData) = getItems ItemType.Shield player - let attacks player = + let getAttacks player = player.Actions |> Array.filter (fun act -> match act.Type with Attack _ -> true | _ -> false) let getDefenses player = player.Actions |> Array.filter (fun act -> match act.Type with Defense -> true | _ -> false) @@ -76,3 +81,34 @@ module Player = let getAttacksFlat actions = actions |> Array.choose (fun act -> match act.Type with Attack ar -> Some (act,ar.Target,ar.Result) | Defense -> None) +module Arsenal = + 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" + | _ -> + let hacks , defenses = actions |> Array.partition (fun act -> match act.Type with Attack _ -> true | Defense -> false) + let hacks = hacks |> Array.take (min hacks.Length 10) + hacks + |> Array.append defenses + |> Array.map (fun act -> + let item = Armory.getItem act.ActionId + match act.Type with + | Attack atk -> + let cooldown = Messaging.getTimeText false Game.SameTargetAttackCooldown act.Timestamp + $"Hacked {atk.Target.Name} {cooldown} ago" + | Defense -> + let cooldown = Messaging.getTimeText true (System.TimeSpan.FromMinutes(int item.Cooldown)) act.Timestamp + $"{item.Name} Shield active for {cooldown}") + |> String.concat "\n" + + let statusFormat p = + $"**Hacks:** {Player.getHacks p |> battleItemFormat}\n + **Shields:** {Player.getShields p |> battleItemFormat}\n + **Hack Attacks:**\n{Player.getAttacks p |> actionFormat}\n + **Active Shields:**\n{Player.getDefenses p |> actionFormat}" + diff --git a/Bot/GuildEnvironment.fs b/Bot/GuildEnvironment.fs index dde7a9e..06bc84a 100644 --- a/Bot/GuildEnvironment.fs +++ b/Bot/GuildEnvironment.fs @@ -2,6 +2,7 @@ module Degenz.GuildEnvironment open System +open DSharpPlus.Entities open dotenv.net DotEnv.Load(DotEnvOptions(envFilePaths = [ "../../../../stag.env" ], overwriteExistingVars = false)) @@ -18,6 +19,9 @@ let channelEventsHackerBattle = getId "CHANNEL_EVENTS_HACKER_BATTLE" let channelTraining = getId "CHANNEL_TRAINING" let channelArmory = getId "CHANNEL_ARMORY" let channelBattle = getId "CHANNEL_BATTLE" -let botHackerBattle = getId "BOT_HACKER_BATTLE" -let botArmory = getId "BOT_ARMORY" +let botIdHackerBattle = getId "BOT_HACKER_BATTLE" +let botIdArmory = getId "BOT_ARMORY" let roleTrainee = getId "ROLE_TRAINEE" + +let mutable botUserHackerBattle : DiscordUser option = None +let mutable botUserArmory : DiscordUser option = None diff --git a/Bot/HackerBattle.fs b/Bot/HackerBattle.fs index 5c214dc..3ae8995 100644 --- a/Bot/HackerBattle.fs +++ b/Bot/HackerBattle.fs @@ -42,7 +42,7 @@ let checkItemHasCooldown itemId attacker = Error $"{item.Name} is currently on cooldown, wait {cooldown} to use it again." let checkHasEmptyHacks attacker = - match Player.hacks attacker with + 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 @@ -94,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 hack) + let embed = Embeds.responseSuccessfulHack true defender.DiscordId (Armory.getItem hack) do! event.Interaction.CreateFollowupMessageAsync(embed) |> Async.AwaitTask |> Async.Ignore @@ -114,7 +114,7 @@ let failedHack (event : ComponentInteractionCreateEventArgs) attacker defender h do! updateCombatants attacker defender hack -Game.ShieldPrize let builder = DiscordMessageBuilder() - builder.WithContent($"Hacking attempt failed! {defender.Name} defended hack from {event.User.Username} and stole {Game.ShieldPrize} $GBT from them! ") |> ignore + 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 @@ -224,6 +224,20 @@ let handleButtonEvent (_ : DiscordClient) (event : ComponentInteractionCreateEve |> 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 () @@ -247,6 +261,10 @@ type HackerGame() = 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) diff --git a/Bot/Store.fs b/Bot/Store.fs index 25eef98..0d9433d 100644 --- a/Bot/Store.fs +++ b/Bot/Store.fs @@ -29,51 +29,6 @@ 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" - | _ -> - let hacks , defenses = actions |> Array.partition (fun act -> match act.Type with Attack _ -> true | Defense -> false) - let hacks = hacks |> Array.take (min hacks.Length 10) - hacks - |> Array.append defenses - |> 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.getShields p |> battleItemFormat}\n -**Hack Attacks:**\n{Player.attacks p |> actionFormat}\n -**Active Shields:**\n{Player.getDefenses 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 false player - let builder = DiscordFollowupMessageBuilder() - let embed = DiscordEmbedBuilder() - embed.AddField("Arsenal", statusFormat updatedPlayer) |> ignore - builder.AddEmbed(embed) |> ignore - builder.IsEphemeral <- true - do! ctx.FollowUpAsync(builder) - |> Async.AwaitTask - |> Async.Ignore - do! DbService.updatePlayer updatedPlayer - }) - let buy itemType (ctx : InteractionContext) = Game.executePlayerInteraction ctx (fun player -> async { let itemStore = Embeds.getBuyItemsEmbed player itemType Armory.battleItems @@ -155,9 +110,6 @@ type Store() = do! Messaging.sendSimpleResponse ctx msg } - [] - member this.Arsenal (ctx : InteractionContext) = arsenal ctx - [] member _.BuyHack (ctx : InteractionContext) = enforceChannel ctx (buy ItemType.Hack) diff --git a/Bot/Trainer.fs b/Bot/Trainer.fs index f0bab31..a558818 100644 --- a/Bot/Trainer.fs +++ b/Bot/Trainer.fs @@ -1,5 +1,6 @@ module Degenz.Trainer +open System.Text open DSharpPlus open DSharpPlus.Entities open DSharpPlus.EventArgs @@ -7,6 +8,8 @@ open DSharpPlus.SlashCommands open Degenz.Types open Degenz.Messaging +let trainerAchievement = "FINISHED_TRAINER" + // TODO: We should either gift the weapons to the player during training, or have them buy it // TODO: We should tell the user to type out /arsenal during the training // TODO: How do we handle the money being generated here? It's fake but fake is confusing @@ -34,7 +37,7 @@ let handleTrainerStep1 (event : ComponentInteractionCreateEventArgs) = Game.executePlayerEvent event (fun player -> async { let shieldMessage , weaponName = 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 + then $"You do not have any Shields in your arsenal, take the `{defaultShield.Name}` shield, you can use it for now.\n\n" , defaultShield.Name else 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 @@ -45,8 +48,7 @@ let handleTrainerStep1 (event : ComponentInteractionCreateEventArgs) = |> Async.AwaitTask do! sendFollowUpMessage event - ("Beautopia© is a dangerous place...\n" - + "Quick, put up a SHIELD 🛡 before another Degen hacks you, and steals your 💰$GBT.\n\n" + ("Beautopia© is a dangerous place... quick, put up a SHIELD 🛡 before another Degen hacks you, and steals your 💰$GBT.\n\n" + shieldMessage + "To enable it, you need to run the `/shield` slash command.\n\n" + $"Type the `/shield` command now, then select - `{weaponName}`\n") @@ -66,13 +68,12 @@ let defend (ctx : InteractionContext) = |> Async.Ignore }) -let handleDefenseMsg = { +let handleDefenseMsg shieldId hackId = { ButtonId = "Trainer-3" ButtonText = "Got it" - Message = "🎉 Congratulations\n\n" - + "You successfully defended my hack!\n\n" - + "Because I tried hacking you when you had your defense up, you stole money from me...\n" - + "Defenses only protect you for a LIMITED TIME, so remember to keep at least 1 defense up at all times, or risk getting hacked!" + Message = "🎉 Congratulations you successfully defended my hack!\n\n" + + $"The `{shieldId}` shield is strong against the `{hackId}` hack, so make sure to have multiple shields up to protect from multiple attacks..." + + "Shields only protect you for a LIMITED TIME, so remember to keep them mounted at all times, or risk getting hacked!" } let handleDefense (event : ComponentInteractionCreateEventArgs) = @@ -81,43 +82,45 @@ let handleDefense (event : ComponentInteractionCreateEventArgs) = let split = event.Id.Split("-") let shieldId = enum(int split.[2]) let shield = Armory.getItem (int shieldId) - let embed = Embeds.responseCreatedShieldTrainer shield + let embed = Embeds.responseCreatedShield (shield) do! event.Interaction.CreateFollowupMessageAsync(embed) |> Async.AwaitTask |> Async.Ignore do! Async.Sleep 4000 - do! sendMessage' "Ok, good, let me make sure that worked.\n\nI'll try to **hack** you now" + let weakHack = Game.getGoodAgainst shield.Class + do! sendMessage' $"Ok, good, let me make sure that worked.\n\nI'll try to **hack** you now with **{snd weakHack}**" do! Async.Sleep 5000 - do! sendMessage' $"❌ HACKING FAILED!\n\n{player.Name} defended the hack from <@{GuildEnvironment.botHackerBattle}>, and stole 💰$GBT {Game.ShieldPrize} from them!" + do! sendMessage' $"❌ HACKING FAILED!\n\n{player.Name} defended hack from <@{GuildEnvironment.botIdHackerBattle}>!" do! Async.Sleep 4000 - do! sendFollowUpMessageWithButton event handleDefenseMsg - }) + do! sendFollowUpMessageWithButton event (handleDefenseMsg shieldId (snd weakHack)) + }) let handleTrainerStep3 (event : ComponentInteractionCreateEventArgs) = Game.executePlayerEvent event (fun player -> async { let hackMessage , weaponName = - if Player.hacks player |> Array.isEmpty + if Player.getHacks player |> Array.isEmpty then $"You do not have any Hacks in your arsenal, take this `{defaultHack.Name}`, you can use it for now.\n\n" , defaultHack.Name else - let name = Player.hacks player |> Array.tryHead |> Option.defaultValue defaultHack |> fun w -> w.Name + let name = Player.getHacks player |> Array.tryHead |> Option.defaultValue defaultHack |> fun w -> w.Name $"Looks like you have `{name}` in your arsenal...\n\n" , name do! sendFollowUpMessage event ("Now let’s **HACK!** 💻\n\n" - + "I want you to **HACK ME**, and try to steal my 💰$GBT...\n\n" + + "I want you to **HACK ME**...\n\n" + hackMessage + "To deploy it, you need to run the `/hack` slash command.\n" - + $"Type the `/hack` command now, then choose me - <@{GuildEnvironment.botHackerBattle}> as your target, and select `{weaponName}`") + + $"Type the `/hack` command now, then choose me - <@{GuildEnvironment.botIdHackerBattle}> as your target, and select `{weaponName}`") }) let attack (target : DiscordUser) (ctx : InteractionContext) = Game.executePlayerInteraction ctx (fun player -> async { - let isRightTarget = target.Id = GuildEnvironment.botHackerBattle + let isRightTarget = target.Id = GuildEnvironment.botIdHackerBattle match isRightTarget with | true -> let playerWithAttacks = - match Player.hacks player with + match Player.getHacks player with | [||] -> { player with Arsenal = [| defaultHack |] } | _ -> player - let embed = Embeds.pickHack "Trainer-4" playerWithAttacks player true + let bot = { player with DiscordId = GuildEnvironment.botIdHackerBattle ; Name = "Sensei" } + let embed = Embeds.pickHack "Trainer-4" playerWithAttacks bot true do! ctx.FollowUpAsync(embed) |> Async.AwaitTask @@ -137,8 +140,8 @@ let handleAttack (event : ComponentInteractionCreateEventArgs) = Game.executePlayerEvent event (fun player -> async { let sendMessage' = sendFollowUpMessage event do! Async.Sleep 1000 - let hack = Player.hacks player |> Array.tryHead |> Option.defaultValue defaultHack - let embed = Embeds.responseSuccessfulHack "Sensei" hack + let hack = Player.getHacks player |> Array.tryHead |> Option.defaultValue defaultHack + let embed = Embeds.responseSuccessfulHack false GuildEnvironment.botIdHackerBattle hack do! event.Interaction.CreateFollowupMessageAsync(embed) |> Async.AwaitTask |> Async.Ignore do! Async.Sleep 5000 do! sendMessage' @@ -149,16 +152,85 @@ let handleAttack (event : ComponentInteractionCreateEventArgs) = do! Async.Sleep 6000 - let membr = event.User :?> DiscordMember - let role = event.Guild.GetRole(GuildEnvironment.roleTrainee) - do! membr.RevokeRoleAsync(role) + let sb = StringBuilder("Here, ") + + let! achievements = DbService.getAchievements player.DiscordId + let isFirstTrainer = achievements |> Option.map (Seq.contains trainerAchievement >> not) |> Option.defaultValue true + if isFirstTrainer then + do! DbService.addAchievement player.DiscordId trainerAchievement + + let hasHacks = Player.getHacks player |> Array.isEmpty |> not + let hasShields = Player.getShields player |> Array.isEmpty |> not + + let rand = System.Random(System.Guid.NewGuid().GetHashCode()) + let freeHack = Armory.hacks.[rand.Next(0, 3)] + let freeShield = Armory.shields.[rand.Next(0, 3)] + let hackMoney = if hasHacks then defaultHack.Cost else 0 + let shieldMoney = if hasShields then defaultShield.Cost else 0 + + let giftMsg = + match hasHacks , hasShields with + | true , true -> $"I'm going to give you these {hackMoney + shieldMoney} 💰$GBT" + | false , true -> $"I'm going to gift you a hack, `{freeHack.Name}` and {defaultHack.Cost} 💰$GBT" + | true , false -> $"I'm going to gift you a shield, `{freeShield.Name}` and {defaultHack.Cost} 💰$GBT" + | false , false -> $"I'm going to gift you a hack,`{freeHack.Name}` and a shield, `{freeShield.Name}`" + + sb.Append(giftMsg) |> ignore + sb.Append(", you'll need em to survive\n\n") |> ignore + sb.AppendLine("To finish your training and collect the loot, type the `/arsenal` command **NOW**") |> ignore + do! Async.Sleep 1000 + let updatedPlayer = { + player with Bank = player.Bank + hackMoney + shieldMoney + Actions = [ + { Action.Timestamp = System.DateTime.UtcNow + Action.Type = + Attack { + Result = true + Target = { Id = GuildEnvironment.botIdHackerBattle ; Name = "Sensei" } + } + ActionId = defaultHack.Id + } + if not hasShields then { + Action.Timestamp = System.DateTime.UtcNow + Action.Type = Defense + Action.ActionId = freeShield.Id + } + ] |> Seq.toArray + |> Array.append player.Actions + Arsenal = [ + if not hasHacks then freeHack + if not hasShields then freeShield + ] |> Seq.toArray + |> Array.append player.Arsenal + } + do! DbService.updatePlayer updatedPlayer + do! sendFollowUpMessage event (sb.ToString()) + else + do! sendFollowUpMessage event ($"Your training is now complete. If you want to buy more **HACKS & SHIELDS**, go to the <#{GuildEnvironment.channelArmory}> **NOW** and type the `/buy-hack` and `/buy-shield` commands! 😱") + let role = event.Guild.GetRole(GuildEnvironment.roleTrainee) + let ``member`` = event.User :?> DiscordMember + do! ``member``.RevokeRoleAsync(role) + |> Async.AwaitTask + }) + +let handleArsenal (ctx : InteractionContext) = + Game.executePlayerInteraction ctx (fun player -> async { + let updatedPlayer = Player.removeExpiredActions false player + let embed = Embeds.getArsenalEmbed updatedPlayer + do! ctx.FollowUpAsync(embed) + |> Async.AwaitTask + |> Async.Ignore + do! Async.Sleep 3000 + let embed = Embeds.getAchievementEmbed player "You completed the Training Dojo and collected loot." trainerAchievement + do! ctx.FollowUpAsync(embed) + |> Async.AwaitTask + |> Async.Ignore + do! Async.Sleep 2000 + let role = ctx.Guild.GetRole(GuildEnvironment.roleTrainee) + do! ctx.Member.RevokeRoleAsync(role) |> Async.AwaitTask - do! sendFollowUpMessage event - ("Your training is **complete!**\n\n" - + "But, you’re going to need more **HACKS & SHIELDS** 🛡 to survive in **Beautopia©...**\n" - + $"You can purchase them from <@{GuildEnvironment.botArmory}> anytime you want...\n\n" - + $"Go to the <#{GuildEnvironment.channelArmory}> **NOW** and type the `/buy-hack` slash command! 😱") + do! sendFollowUpMessageFromCtx ctx "Now get out of there and go hack other Degenz!" }) let handleButtonEvent (event : ComponentInteractionCreateEventArgs) = diff --git a/DbService/DbService.fs b/DbService/DbService.fs index 22aa8e4..0579774 100644 --- a/DbService/DbService.fs +++ b/DbService/DbService.fs @@ -2,11 +2,10 @@ open System open System.Collections.Generic -open System.Threading.Tasks -open Degenz.Types open MongoDB.Bson open MongoDB.Bson.Serialization open MongoDB.Driver +open Degenz.Types [] type AttackAction = @@ -90,6 +89,28 @@ let tryFindPlayer (id : uint64) = |> Some } +let getAchievements (id : uint64) = + async { + let filter = Builders.Filter.Eq("Player.DiscordId", id) + try + let! player = players.FindAsync(filter) |> Async.AwaitTask + match player.FirstOrDefault() with + | null -> return None + | p -> return p + .GetValue("achievements") + .AsBsonArray + |> Seq.map (fun (bv : BsonValue) -> bv.AsString) + |> Some + with ex -> return None + } + +let addAchievement (id : uint64) (achievement : string) = + async { + let filter = Builders.Filter.Eq("Player.DiscordId", id) + let update = Builders.Update.Push("achievements", achievement) + return! players.UpdateOneAsync(filter, update) |> Async.AwaitTask |> Async.Ignore + } + let insertNewPlayer (player : PlayerData) = async { let p = playerMap player @@ -114,7 +135,6 @@ let updatePlayer (player : PlayerData) = return! players.UpdateOneAsync(filter, update) |> Async.AwaitTask |> Async.Ignore } - //let getTopPlayers amount = // async { // return! players.FindAsync() diff --git a/Shared/Shared.fs b/Shared/Shared.fs index 3576f33..04f4a73 100644 --- a/Shared/Shared.fs +++ b/Shared/Shared.fs @@ -71,7 +71,7 @@ module Types = type Action = { ActionId : int Type : ActionType - Timestamp : DateTime } + Timestamp : System.DateTime } [] type PlayerData = @@ -86,6 +86,9 @@ module Armory = let file = System.IO.File.ReadAllText("Items.json") JsonConvert.DeserializeObject(file) + let hacks = battleItems |> Array.filter (fun bi -> match bi.Type with Hack -> true | Shield -> false) + let shields = battleItems |> Array.filter (fun bi -> match bi.Type with Shield -> true | Hack -> false) + let getItem itemId = battleItems |> Array.find (fun w -> w.Id = itemId) module Messaging =