From 9f80069b2f616380387d438e441fc34a7a9058ff Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 26 Feb 2022 21:28:02 +0700 Subject: [PATCH 01/28] Single project sln structure --- Bot/Bot.fsproj | 7 +- {DbService => Bot}/DbService.fs | 0 Bot/Game.fs | 308 +++++++++++++++++++++++++++----- Bot/HackerBattle.fs | 10 +- Bot/PlayerInteractions.fs | 72 +++++--- Bot/Prelude.fs | 22 +++ Bot/RockPaperScissors.fs | 4 +- Bot/SlotMachine.fs | 2 +- Bot/Store.fs | 8 +- Bot/Thief.fs | 4 +- Bot/Trainer.fs | 4 +- DbService/DbService.fsproj | 3 - DegenzGame.sln | 12 -- Shared/Shared.fs | 274 ---------------------------- 14 files changed, 351 insertions(+), 379 deletions(-) rename {DbService => Bot}/DbService.fs (100%) create mode 100644 Bot/Prelude.fs diff --git a/Bot/Bot.fsproj b/Bot/Bot.fsproj index 6d23b5c..2def2ea 100644 --- a/Bot/Bot.fsproj +++ b/Bot/Bot.fsproj @@ -11,10 +11,12 @@ PreserveNewest + - + + @@ -24,8 +26,5 @@ - - - \ No newline at end of file diff --git a/DbService/DbService.fs b/Bot/DbService.fs similarity index 100% rename from DbService/DbService.fs rename to Bot/DbService.fs diff --git a/Bot/Game.fs b/Bot/Game.fs index 9ccd028..0b85933 100644 --- a/Bot/Game.fs +++ b/Bot/Game.fs @@ -1,10 +1,268 @@ namespace Degenz +open System open System.Threading.Tasks open DSharpPlus open DSharpPlus.Entities -open Degenz.DbService -open Degenz.Messaging +open DSharpPlus.EventArgs +open DSharpPlus.SlashCommands +open Newtonsoft.Json + +[] +module Types = + + [] + type mins + + [] + type GBT + + type HackId = + | Virus = 0 + | RemoteAccess = 1 + | Worm = 2 + + type ShieldId = + | Firewall = 6 + | Encryption = 7 + | Cypher = 8 + + type ItemType = + | Hack = 0 + | Shield = 1 + | Food = 1 + + type ItemAttributes = { + Sell : bool + Buy : bool + Consume : bool + Drop : bool + } + with static member empty = { Sell = false ; Buy = false ; Consume = false ; Drop = false } + + type Item = { + Id : int + Name : string + Price : int + Type : ItemType + Power : int + Class : int + Cooldown : int + Attributes : ItemAttributes + } + with static member empty = + { Id = -1 + Name = "None" + Price = 0 + Type = ItemType.Hack + Power = 0 + Class = -1 + Cooldown = 0 + Attributes = ItemAttributes.empty } + + type HackResult = + | Strong + | Weak + + [] + type DiscordPlayer = { Id: uint64; Name: string } + with static member empty = { Id = 0uL ; Name = "None" } + + type HackEvent = { + IsInstigator : bool + Adversary : DiscordPlayer + Success : bool + HackId : int + } + + type PlayerEventType = + | Hacking of HackEvent + | Shielding of shieldId : int + | Stealing of instigator : bool * adversary : DiscordPlayer + | Imprison + + type PlayerEvent = + { Type : PlayerEventType + Cooldown : int + Timestamp : DateTime } + + type PlayerTraits = { + Strength : int + Focus : int + Luck : int + Charisma : int + } + with static member empty = { Strength = 0 ; Focus = 0 ; Luck = 0 ; Charisma = 0 } + + [] + type PlayerData = { + DiscordId : uint64 + Name : string + Inventory : Item array + Events : PlayerEvent array + Traits : PlayerTraits + Bank : int + } +// Achievements : string array +// XP : int + with member this.basicPlayer = { Id = this.DiscordId ; Name = this.Name } + static member empty = + { DiscordId = 0uL + Name = "None" + Inventory = [||] + Events = [||] + Traits = PlayerTraits.empty +// Achievements = [||] +// XP = 0 + Bank = 0 } + +module Armory = + let battleItems = + let file = System.IO.File.ReadAllText("Items.json") +// let file = System.IO.File.ReadAllText("Bot/Items.json") + JsonConvert.DeserializeObject(file) + + let hacks = battleItems |> Array.filter (fun bi -> match bi.Type with ItemType.Hack -> true | _ -> false) + let shields = battleItems |> Array.filter (fun bi -> match bi.Type with ItemType.Shield -> true | _ -> false) + + let getItem itemId = battleItems |> Array.find (fun w -> w.Id = itemId) + +module Messaging = + type InteractiveMessage = { + ButtonId : string + ButtonText : string + Message : string + } + + type DiscordContext = + | Interaction of InteractionContext + | Event of ComponentInteractionCreateEventArgs + + type IDiscordContext = + abstract member Respond : InteractionResponseType -> Task + abstract member Respond : InteractionResponseType * DiscordInteractionResponseBuilder -> Task + abstract member FollowUp : DiscordFollowupMessageBuilder -> Task + abstract member GetDiscordMember : unit -> DiscordMember + abstract member GetGuild : unit -> DiscordGuild + abstract member GetInteractionId : unit -> string + abstract member GetChannel : unit -> DiscordChannel + abstract member GetContext : unit -> obj + + type DiscordInteractionContext(ctx : InteractionContext) = + interface IDiscordContext with + member this.Respond responseType = + async { + do! ctx.Interaction.CreateResponseAsync(responseType) |> Async.AwaitTask + } |> Async.StartAsTask :> Task + member this.Respond (responseType, builder) = + async { + do! ctx.Interaction.CreateResponseAsync(responseType, builder) |> Async.AwaitTask + } |> Async.StartAsTask :> Task + member this.FollowUp(builder) = + async { + do! ctx.Interaction.CreateFollowupMessageAsync(builder) |> Async.AwaitTask |> Async.Ignore + } |> Async.StartAsTask :> Task + member this.GetDiscordMember() = ctx.Member + member this.GetGuild() = ctx.Guild + member this.GetInteractionId() = string ctx.InteractionId + member this.GetChannel() = ctx.Channel + member this.GetContext() = ctx + + type DiscordEventContext(ctx : ComponentInteractionCreateEventArgs) = + interface IDiscordContext with + member this.Respond responseType = + async { + do! ctx.Interaction.CreateResponseAsync(responseType) |> Async.AwaitTask + } |> Async.StartAsTask :> Task + member this.Respond (responseType, builder) = + async { + do! ctx.Interaction.CreateResponseAsync(responseType, builder) |> Async.AwaitTask + } |> Async.StartAsTask :> Task + member this.FollowUp(builder) = + async { + do! ctx.Interaction.CreateFollowupMessageAsync(builder) |> Async.AwaitTask |> Async.Ignore + } |> Async.StartAsTask :> Task + member this.GetDiscordMember() = ctx.User :?> DiscordMember + member this.GetGuild() = ctx.Guild + member this.GetInteractionId() = ctx.Id + member this.GetChannel() = ctx.Channel + member this.GetContext() = ctx + + 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 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 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 defer (ctx: IDiscordContext) = async { + let builder = DiscordInteractionResponseBuilder() + builder.IsEphemeral <- true + do! ctx.Respond(InteractionResponseType.DeferredChannelMessageWithSource, builder) |> Async.AwaitTask + } + + let sendSimpleResponse (ctx: IDiscordContext) msg = + async { + let builder = DiscordInteractionResponseBuilder() + builder.Content <- msg + builder.AsEphemeral true |> ignore + do! ctx.Respond(InteractionResponseType.ChannelMessageWithSource, builder) |> Async.AwaitTask + } + + let sendFollowUpMessage (ctx : IDiscordContext) msg = + async { + let builder = DiscordFollowupMessageBuilder() + builder.IsEphemeral <- true + builder.Content <- msg + do! ctx.FollowUp(builder) |> Async.AwaitTask + } + + let sendFollowUpEmbed (ctx : IDiscordContext) embed = + async { + let builder = + DiscordFollowupMessageBuilder() + .AsEphemeral(true) + .AddEmbed(embed) + do! ctx.FollowUp(builder) |> Async.AwaitTask + } + + let sendFollowUpMessageWithButton (ctx : IDiscordContext) interactiveMessage = + async { + let builder = DiscordFollowupMessageBuilder() + let button = DiscordButtonComponent(ButtonStyle.Success, interactiveMessage.ButtonId, interactiveMessage.ButtonText) :> DiscordComponent + builder.AddComponents [| button |] |> ignore + builder.IsEphemeral <- true + builder.Content <- interactiveMessage.Message + do! ctx.FollowUp(builder) |> Async.AwaitTask + } + + let updateMessageWithGreyedOutButtons (ctx : IDiscordContext) interactiveMessage = + async { + let builder = DiscordInteractionResponseBuilder() + let button = DiscordButtonComponent(ButtonStyle.Success, interactiveMessage.ButtonId, interactiveMessage.ButtonText, true) :> DiscordComponent + builder.AddComponents [| button |] |> ignore + builder.IsEphemeral <- true + builder.Content <- interactiveMessage.Message + do! ctx.Respond(InteractionResponseType.UpdateMessage, builder) |> Async.AwaitTask + } + + let handleResultWithResponse ctx fn (player : Result) = + match player with + | Ok p -> fn p + | Error e -> async { do! sendFollowUpMessage ctx e } + +open Messaging module Game = let SameTargetAttackCooldown = System.TimeSpan.FromHours(1) @@ -24,52 +282,6 @@ module Game = | 1 -> ( ShieldId.Encryption , HackId.RemoteAccess ) | _ -> ( ShieldId.Cypher , HackId.Worm ) - let executePlayerAction (ctx : IDiscordContext) (dispatch : PlayerData -> Async) = - async { - let builder = DiscordInteractionResponseBuilder().AsEphemeral(true) - do! ctx.Respond(InteractionResponseType.DeferredChannelMessageWithSource, builder) |> Async.AwaitTask - let! playerResult = tryFindPlayer GuildEnvironment.pgDb (ctx.GetDiscordMember().Id) - match playerResult with - | Some player -> do! dispatch player - | None -> do! Messaging.sendFollowUpMessage ctx "You are currently not a hacker, first use the /redpill command to become one" - } |> Async.StartAsTask :> Task - - let executePlayerActionWithTarget (targetPlayer : DiscordUser) (ctx : IDiscordContext) (dispatch : PlayerData -> PlayerData -> Async) = - async { - let builder = DiscordInteractionResponseBuilder() - builder.IsEphemeral <- true - builder.Content <- "Content" - do! ctx.Respond(InteractionResponseType.DeferredChannelMessageWithSource, builder) |> Async.AwaitTask - let! players = - [ tryFindPlayer GuildEnvironment.pgDb (ctx.GetDiscordMember().Id) - tryFindPlayer GuildEnvironment.pgDb targetPlayer.Id ] - |> Async.Parallel - match players.[0] , players.[1] with - | Some player , Some target -> do! dispatch player target - | None , _ -> do! Messaging.sendFollowUpMessage ctx "You are currently not a hacker, first use the /redpill command to become one" - | _ , None -> - if targetPlayer.IsBot - then do! Messaging.sendFollowUpMessage ctx $"{targetPlayer.Username} is a bot, pick a real human to hack" - else do! Messaging.sendFollowUpMessage ctx "Your target is not connected to the network, they must join first by using the /redpill command" - } |> Async.StartAsTask :> Task - - let executePlayerActionWithTargetId defer (targetId : uint64) (ctx : IDiscordContext) (dispatch : PlayerData -> PlayerData -> Async) = - async { - let builder = DiscordInteractionResponseBuilder() - builder.IsEphemeral <- true - builder.Content <- "Content" - if defer then - do! ctx.Respond(InteractionResponseType.DeferredChannelMessageWithSource, builder) |> Async.AwaitTask - let! players = - [ tryFindPlayer GuildEnvironment.pgDb (ctx.GetDiscordMember().Id) - tryFindPlayer GuildEnvironment.pgDb targetId ] - |> Async.Parallel - match players.[0] , players.[1] with - | Some player , Some target -> do! dispatch player target - | None , _ -> do! Messaging.sendFollowUpMessage ctx "You are currently not a hacker, first use the /redpill command to become one" - | _ , None -> do! Messaging.sendFollowUpMessage ctx "Your target is not connected to the network, they must join first by using the /redpill command" - } |> Async.StartAsTask :> Task - module Player = let getItems itemType (player : PlayerData) = player.Inventory |> Array.filter (fun i -> i.Type = itemType) let getHacks (player : PlayerData) = getItems ItemType.Hack player diff --git a/Bot/HackerBattle.fs b/Bot/HackerBattle.fs index 27b870f..f58dd28 100644 --- a/Bot/HackerBattle.fs +++ b/Bot/HackerBattle.fs @@ -136,7 +136,7 @@ let failedHack (ctx : IDiscordContext) attacker defender hack = } let hack (target : DiscordUser) (ctx : IDiscordContext) = - Game.executePlayerActionWithTarget target ctx (fun attacker defender -> async { + PlayerInteractions.executePlayerActionWithTarget target ctx (fun attacker defender -> async { do! attacker |> Player.removeExpiredActions |> checkAlreadyHackedTarget defender @@ -151,7 +151,7 @@ let hack (target : DiscordUser) (ctx : IDiscordContext) = }) let handleAttack (ctx : IDiscordContext) = - Game.executePlayerAction ctx (fun attacker -> async { + PlayerInteractions.executePlayerAction ctx (fun attacker -> async { let tokens = ctx.GetInteractionId().Split("-") let hackId = int tokens.[1] let hack = Armory.getItem hackId @@ -176,7 +176,7 @@ let handleAttack (ctx : IDiscordContext) = }) let defend (ctx : IDiscordContext) = - Game.executePlayerAction ctx (fun player -> async { + PlayerInteractions.executePlayerAction ctx (fun player -> async { if Player.getShields player |> Array.length > 0 then let p = Player.removeExpiredActions player let embed = Embeds.pickDefense "Defend" p false @@ -187,7 +187,7 @@ let defend (ctx : IDiscordContext) = }) let handleDefense (ctx : IDiscordContext) = - Game.executePlayerAction ctx (fun player -> async { + PlayerInteractions.executePlayerAction ctx (fun player -> async { let tokens = ctx.GetInteractionId().Split("-") let shieldId = int tokens.[1] let shield = Armory.getItem shieldId @@ -218,7 +218,7 @@ let handleDefense (ctx : IDiscordContext) = }) let arsenal (ctx : IDiscordContext) = - Game.executePlayerAction ctx (fun player -> async { + PlayerInteractions.executePlayerAction ctx (fun player -> async { let updatedPlayer = Player.removeExpiredActions player let builder = DiscordFollowupMessageBuilder() let embed = DiscordEmbedBuilder() diff --git a/Bot/PlayerInteractions.fs b/Bot/PlayerInteractions.fs index 817006c..3122c8c 100644 --- a/Bot/PlayerInteractions.fs +++ b/Bot/PlayerInteractions.fs @@ -1,8 +1,57 @@ module Degenz.PlayerInteractions open System.Threading.Tasks +open DSharpPlus +open DSharpPlus.Entities open DSharpPlus.SlashCommands -open Degenz.Types +open Degenz.Messaging +open Degenz.DbService + +let executePlayerAction (ctx : IDiscordContext) (dispatch : PlayerData -> Async) = + async { + let builder = DiscordInteractionResponseBuilder().AsEphemeral(true) + do! ctx.Respond(InteractionResponseType.DeferredChannelMessageWithSource, builder) |> Async.AwaitTask + let! playerResult = tryFindPlayer GuildEnvironment.pgDb (ctx.GetDiscordMember().Id) + match playerResult with + | Some player -> do! dispatch player + | None -> do! Messaging.sendFollowUpMessage ctx "You are currently not a hacker, first use the /redpill command to become one" + } |> Async.StartAsTask :> Task + +let executePlayerActionWithTarget (targetPlayer : DiscordUser) (ctx : IDiscordContext) (dispatch : PlayerData -> PlayerData -> Async) = + async { + let builder = DiscordInteractionResponseBuilder() + builder.IsEphemeral <- true + builder.Content <- "Content" + do! ctx.Respond(InteractionResponseType.DeferredChannelMessageWithSource, builder) |> Async.AwaitTask + let! players = + [ tryFindPlayer GuildEnvironment.pgDb (ctx.GetDiscordMember().Id) + tryFindPlayer GuildEnvironment.pgDb targetPlayer.Id ] + |> Async.Parallel + match players.[0] , players.[1] with + | Some player , Some target -> do! dispatch player target + | None , _ -> do! Messaging.sendFollowUpMessage ctx "You are currently not a hacker, first use the /redpill command to become one" + | _ , None -> + if targetPlayer.IsBot + then do! Messaging.sendFollowUpMessage ctx $"{targetPlayer.Username} is a bot, pick a real human to hack" + else do! Messaging.sendFollowUpMessage ctx "Your target is not connected to the network, they must join first by using the /redpill command" + } |> Async.StartAsTask :> Task + +let executePlayerActionWithTargetId defer (targetId : uint64) (ctx : IDiscordContext) (dispatch : PlayerData -> PlayerData -> Async) = + async { + let builder = DiscordInteractionResponseBuilder() + builder.IsEphemeral <- true + builder.Content <- "Content" + if defer then + do! ctx.Respond(InteractionResponseType.DeferredChannelMessageWithSource, builder) |> Async.AwaitTask + let! players = + [ tryFindPlayer GuildEnvironment.pgDb (ctx.GetDiscordMember().Id) + tryFindPlayer GuildEnvironment.pgDb targetId ] + |> Async.Parallel + match players.[0] , players.[1] with + | Some player , Some target -> do! dispatch player target + | None , _ -> do! Messaging.sendFollowUpMessage ctx "You are currently not a hacker, first use the /redpill command to become one" + | _ , None -> do! Messaging.sendFollowUpMessage ctx "Your target is not connected to the network, they must join first by using the /redpill command" + } |> Async.StartAsTask :> Task module Commands = let newPlayer nickname (membr : uint64) = @@ -42,27 +91,6 @@ module Commands = Name : string } -// let leaderboard (ctx : InteractionContext) = -// async { -// do! ctx.CreateResponseAsync(InteractionResponseType.DeferredChannelMessageWithSource) |> Async.AwaitTask -// -// let builder = DiscordFollowupMessageBuilder() -// builder.IsEphemeral <- true -// -// let! leaders = DbService.getTopPlayers 10 -// let content = -// leaders -// |> Seq.toArray -// |> Array.sortByDescending (fun p -> p.Bank) -// |> Array.mapi (fun i p -> { Position = string (i + 1) ; Amount = string p.Bank ; Name = p.Name }) -// |> Formatter.Format -// builder.Content <- if not <| String.IsNullOrEmpty content then $"```{content}```" else "There are no active hackers" -// do! ctx.Interaction.CreateFollowupMessageAsync(builder) -// |> Async.AwaitTask -// |> Async.Ignore -// } |> Async.StartAsTask -// :> Task - type PlayerInteractions() = inherit ApplicationCommandModule () diff --git a/Bot/Prelude.fs b/Bot/Prelude.fs new file mode 100644 index 0000000..e224ddf --- /dev/null +++ b/Bot/Prelude.fs @@ -0,0 +1,22 @@ +namespace Degenz + +[] +module ResultHelpers = + let (>>=) x f = Result.bind f x + let () x f = Result.map f x + +[] +[] +module List = + let cons xs x = x :: xs + let consTo x xs = x :: xs + + let rec foldk f (acc:'TState) xs = + match xs with + | [] -> acc + | x::xs -> f acc x (fun lacc -> foldk f lacc xs) + + let foldi (f : int -> 'Acc -> 'elem -> 'Acc) (acc : 'Acc) xs = + let f' ( i , st ) acc = ( i + 1 , f i st acc ) + List.fold f' ( 0 , acc ) xs |> snd + diff --git a/Bot/RockPaperScissors.fs b/Bot/RockPaperScissors.fs index 38e9487..7b93835 100644 --- a/Bot/RockPaperScissors.fs +++ b/Bot/RockPaperScissors.fs @@ -85,7 +85,7 @@ let matchResultsEmbed winner move1 move2 player1 player2 = [ firstEmbed ; secondEmbed ; thirdEmbed ] let playRPS target ctx = - Game.executePlayerActionWithTarget target ctx (fun _ defender -> async { + PlayerInteractions.executePlayerActionWithTarget target ctx (fun _ defender -> async { let buttons , embed = rpsEmbed false None defender let builder = DiscordFollowupMessageBuilder() @@ -100,7 +100,7 @@ let handleRPS (ctx : IDiscordContext) = let move = tokens.[1] let targetId = uint64 tokens.[2] let isResponse = tokens.[4] = "True" - Game.executePlayerActionWithTargetId false targetId ctx (fun attacker defender -> async { + PlayerInteractions.executePlayerActionWithTargetId false targetId ctx (fun attacker defender -> async { if isResponse then let eventCtx = ctx.GetContext() :?> ComponentInteractionCreateEventArgs let buttons , embed = rpsEmbed true None attacker diff --git a/Bot/SlotMachine.fs b/Bot/SlotMachine.fs index 5c1a2be..b401381 100644 --- a/Bot/SlotMachine.fs +++ b/Bot/SlotMachine.fs @@ -15,7 +15,7 @@ type SlotMachine() = [] member this.Spin (ctx : InteractionContext) = - Game.executePlayerAction (DiscordInteractionContext ctx) (fun player -> async { + PlayerInteractions.executePlayerAction (DiscordInteractionContext ctx) (fun player -> async { let sleepTime = 1000 let random = Random(System.Guid.NewGuid().GetHashCode()) let results = [ random.Next(0, 3) ; random.Next(0, 3) ; random.Next(0, 3)] diff --git a/Bot/Store.fs b/Bot/Store.fs index 6a4a219..3e2efb4 100644 --- a/Bot/Store.fs +++ b/Bot/Store.fs @@ -29,13 +29,13 @@ let checkHasItemsInArsenal itemType player = else Error $"You currently have no {itemType}s in your arsenal to sell!" let buy itemType (ctx : IDiscordContext) = - Game.executePlayerAction ctx (fun player -> async { + PlayerInteractions.executePlayerAction ctx (fun player -> async { let itemStore = Embeds.getBuyItemsEmbed player itemType Armory.battleItems do! ctx.FollowUp itemStore |> Async.AwaitTask }) let sell itemType (ctx : IDiscordContext) = - Game.executePlayerAction ctx (fun player -> async { + PlayerInteractions.executePlayerAction ctx (fun player -> async { match checkHasItemsInArsenal itemType player with | Ok _ -> let itemStore = Embeds.getSellEmbed itemType player do! ctx.FollowUp(itemStore) |> Async.AwaitTask @@ -44,7 +44,7 @@ let sell itemType (ctx : IDiscordContext) = // TODO: When you buy a shield, prompt the user to activate it let handleBuyItem (ctx : IDiscordContext) itemId = - Game.executePlayerAction ctx (fun player -> async { + PlayerInteractions.executePlayerAction ctx (fun player -> async { let item = Armory.battleItems |> Array.find (fun w -> w.Id = itemId) do! player |> checkHasSufficientFunds item @@ -58,7 +58,7 @@ let handleBuyItem (ctx : IDiscordContext) itemId = }) let handleSell (ctx : IDiscordContext) itemId = - Game.executePlayerAction ctx (fun player -> async { + PlayerInteractions.executePlayerAction ctx (fun player -> async { let item = Armory.getItem itemId do! player diff --git a/Bot/Thief.fs b/Bot/Thief.fs index 12e5975..5cdb0fa 100644 --- a/Bot/Thief.fs +++ b/Bot/Thief.fs @@ -119,7 +119,7 @@ let calculateWinPercentage amountRequested bank attackerStrength defenderStrengt //calculateWinPercentage 50 200 100 85 let steal target amount (ctx : IDiscordContext) = - Game.executePlayerActionWithTarget target ctx (fun thief victim -> async { do! + PlayerInteractions.executePlayerActionWithTarget target ctx (fun thief victim -> async { do! thief |> checkPlayerIsAttackingThemselves victim // |> checkVictimStealingCooldown victim @@ -219,7 +219,7 @@ let handleSteal (ctx : IDiscordContext) = } if answer = "yes" then let targetId = uint64 tokens.[2] - Game.executePlayerActionWithTargetId true targetId ctx (fun attacker defender -> async { + PlayerInteractions.executePlayerActionWithTargetId true targetId ctx (fun attacker defender -> async { do! attacker |> Player.removeExpiredActions // |> checkVictimStealingCooldown defender diff --git a/Bot/Trainer.fs b/Bot/Trainer.fs index a2dcc91..781bf57 100644 --- a/Bot/Trainer.fs +++ b/Bot/Trainer.fs @@ -131,7 +131,7 @@ let hack (target : DiscordUser) (ctx : IDiscordContext) = } |> Async.StartAsTask :> Task let handleHack (ctx : IDiscordContext) = - Game.executePlayerAction ctx (fun player -> async { + PlayerInteractions.executePlayerAction ctx (fun player -> async { let sendMessage' = sendFollowUpMessage ctx do! Async.Sleep 1000 let embed = Embeds.responseSuccessfulHack false Sensei.Id defaultHack.Power defaultHack @@ -166,7 +166,7 @@ let handleHack (ctx : IDiscordContext) = }) let handleArsenal (ctx : IDiscordContext) = - Game.executePlayerAction ctx (fun player -> async { + PlayerInteractions.executePlayerAction ctx (fun player -> async { let hasStockWeapons = Player.getHacks player |> Array.exists (fun item -> item.Id = defaultHack.Id) let updatedPlayer = if not hasStockWeapons then { diff --git a/DbService/DbService.fsproj b/DbService/DbService.fsproj index 45a74ae..e7786d7 100644 --- a/DbService/DbService.fsproj +++ b/DbService/DbService.fsproj @@ -4,9 +4,6 @@ net6.0 true - - - diff --git a/DegenzGame.sln b/DegenzGame.sln index 84b9a1a..c77e294 100644 --- a/DegenzGame.sln +++ b/DegenzGame.sln @@ -5,10 +5,6 @@ VisualStudioVersion = 16.0.30114.105 MinimumVisualStudioVersion = 10.0.40219.1 Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Bot", "Bot\Bot.fsproj", "{FF9E58A6-1A1D-4DEC-B52D-265F215BF315}" EndProject -Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "DbService", "DbService\DbService.fsproj", "{B1D3E1CC-451C-42D4-B054-D64E75E1A3B9}" -EndProject -Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Shared", "Shared\Shared.fsproj", "{BD80E85E-87C8-4F5F-941E-7DCAF9D69838}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -22,13 +18,5 @@ Global {FF9E58A6-1A1D-4DEC-B52D-265F215BF315}.Debug|Any CPU.Build.0 = Debug|Any CPU {FF9E58A6-1A1D-4DEC-B52D-265F215BF315}.Release|Any CPU.ActiveCfg = Release|Any CPU {FF9E58A6-1A1D-4DEC-B52D-265F215BF315}.Release|Any CPU.Build.0 = Release|Any CPU - {B1D3E1CC-451C-42D4-B054-D64E75E1A3B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B1D3E1CC-451C-42D4-B054-D64E75E1A3B9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B1D3E1CC-451C-42D4-B054-D64E75E1A3B9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B1D3E1CC-451C-42D4-B054-D64E75E1A3B9}.Release|Any CPU.Build.0 = Release|Any CPU - {BD80E85E-87C8-4F5F-941E-7DCAF9D69838}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BD80E85E-87C8-4F5F-941E-7DCAF9D69838}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BD80E85E-87C8-4F5F-941E-7DCAF9D69838}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BD80E85E-87C8-4F5F-941E-7DCAF9D69838}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/Shared/Shared.fs b/Shared/Shared.fs index 21611f1..8148165 100644 --- a/Shared/Shared.fs +++ b/Shared/Shared.fs @@ -7,277 +7,3 @@ open DSharpPlus.Entities open DSharpPlus.EventArgs open DSharpPlus.SlashCommands open Newtonsoft.Json - -[] -module ResultHelpers = - let (>>=) x f = Result.bind f x - let () x f = Result.map f x - -[] -module List = - let cons xs x = x :: xs - let consTo x xs = x :: xs - - let rec foldk f (acc:'TState) xs = - match xs with - | [] -> acc - | x::xs -> f acc x (fun lacc -> foldk f lacc xs) - - let foldi (f : int -> 'Acc -> 'elem -> 'Acc) (acc : 'Acc) xs = - let f' ( i , st ) acc = ( i + 1 , f i st acc ) - List.fold f' ( 0 , acc ) xs |> snd - -[] -module Types = - - [] - type mins - - [] - type GBT - - type HackId = - | Virus = 0 - | RemoteAccess = 1 - | Worm = 2 - - type ShieldId = - | Firewall = 6 - | Encryption = 7 - | Cypher = 8 - - type ItemType = - | Hack = 0 - | Shield = 1 - | Food = 1 - - type ItemAttributes = { - Sell : bool - Buy : bool - Consume : bool - Drop : bool - } - with static member empty = { Sell = false ; Buy = false ; Consume = false ; Drop = false } - - type Item = { - Id : int - Name : string - Price : int - Type : ItemType - Power : int - Class : int - Cooldown : int - Attributes : ItemAttributes - } - with static member empty = - { Id = -1 - Name = "None" - Price = 0 - Type = ItemType.Hack - Power = 0 - Class = -1 - Cooldown = 0 - Attributes = ItemAttributes.empty } - - type HackResult = - | Strong - | Weak - - [] - type DiscordPlayer = { Id: uint64; Name: string } - with static member empty = { Id = 0uL ; Name = "None" } - - type HackEvent = { - IsInstigator : bool - Adversary : DiscordPlayer - Success : bool - HackId : int - } - - type PlayerEventType = - | Hacking of HackEvent - | Shielding of shieldId : int - | Stealing of instigator : bool * adversary : DiscordPlayer - | Imprison - - type PlayerEvent = - { Type : PlayerEventType - Cooldown : int - Timestamp : DateTime } - - type PlayerTraits = { - Strength : int - Focus : int - Luck : int - Charisma : int - } - with static member empty = { Strength = 0 ; Focus = 0 ; Luck = 0 ; Charisma = 0 } - - [] - type PlayerData = { - DiscordId : uint64 - Name : string - Inventory : Item array - Events : PlayerEvent array - Traits : PlayerTraits - Bank : int - } -// Achievements : string array -// XP : int - with member this.basicPlayer = { Id = this.DiscordId ; Name = this.Name } - static member empty = - { DiscordId = 0uL - Name = "None" - Inventory = [||] - Events = [||] - Traits = PlayerTraits.empty -// Achievements = [||] -// XP = 0 - Bank = 0 } - -module Armory = - let battleItems = - let file = System.IO.File.ReadAllText("Items.json") -// let file = System.IO.File.ReadAllText("Bot/Items.json") - JsonConvert.DeserializeObject(file) - - let hacks = battleItems |> Array.filter (fun bi -> match bi.Type with ItemType.Hack -> true | _ -> false) - let shields = battleItems |> Array.filter (fun bi -> match bi.Type with ItemType.Shield -> true | _ -> false) - - let getItem itemId = battleItems |> Array.find (fun w -> w.Id = itemId) - -module Messaging = - type InteractiveMessage = { - ButtonId : string - ButtonText : string - Message : string - } - - type DiscordContext = - | Interaction of InteractionContext - | Event of ComponentInteractionCreateEventArgs - - type IDiscordContext = - abstract member Respond : InteractionResponseType -> Task - abstract member Respond : InteractionResponseType * DiscordInteractionResponseBuilder -> Task - abstract member FollowUp : DiscordFollowupMessageBuilder -> Task - abstract member GetDiscordMember : unit -> DiscordMember - abstract member GetGuild : unit -> DiscordGuild - abstract member GetInteractionId : unit -> string - abstract member GetChannel : unit -> DiscordChannel - abstract member GetContext : unit -> obj - - type DiscordInteractionContext(ctx : InteractionContext) = - interface IDiscordContext with - member this.Respond responseType = - async { - do! ctx.Interaction.CreateResponseAsync(responseType) |> Async.AwaitTask - } |> Async.StartAsTask :> Task - member this.Respond (responseType, builder) = - async { - do! ctx.Interaction.CreateResponseAsync(responseType, builder) |> Async.AwaitTask - } |> Async.StartAsTask :> Task - member this.FollowUp(builder) = - async { - do! ctx.Interaction.CreateFollowupMessageAsync(builder) |> Async.AwaitTask |> Async.Ignore - } |> Async.StartAsTask :> Task - member this.GetDiscordMember() = ctx.Member - member this.GetGuild() = ctx.Guild - member this.GetInteractionId() = string ctx.InteractionId - member this.GetChannel() = ctx.Channel - member this.GetContext() = ctx - - type DiscordEventContext(ctx : ComponentInteractionCreateEventArgs) = - interface IDiscordContext with - member this.Respond responseType = - async { - do! ctx.Interaction.CreateResponseAsync(responseType) |> Async.AwaitTask - } |> Async.StartAsTask :> Task - member this.Respond (responseType, builder) = - async { - do! ctx.Interaction.CreateResponseAsync(responseType, builder) |> Async.AwaitTask - } |> Async.StartAsTask :> Task - member this.FollowUp(builder) = - async { - do! ctx.Interaction.CreateFollowupMessageAsync(builder) |> Async.AwaitTask |> Async.Ignore - } |> Async.StartAsTask :> Task - member this.GetDiscordMember() = ctx.User :?> DiscordMember - member this.GetGuild() = ctx.Guild - member this.GetInteractionId() = ctx.Id - member this.GetChannel() = ctx.Channel - member this.GetContext() = ctx - - 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 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 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 defer (ctx: IDiscordContext) = async { - let builder = DiscordInteractionResponseBuilder() - builder.IsEphemeral <- true - do! ctx.Respond(InteractionResponseType.DeferredChannelMessageWithSource, builder) |> Async.AwaitTask - } - - let sendSimpleResponse (ctx: IDiscordContext) msg = - async { - let builder = DiscordInteractionResponseBuilder() - builder.Content <- msg - builder.AsEphemeral true |> ignore - do! ctx.Respond(InteractionResponseType.ChannelMessageWithSource, builder) |> Async.AwaitTask - } - - let sendFollowUpMessage (ctx : IDiscordContext) msg = - async { - let builder = DiscordFollowupMessageBuilder() - builder.IsEphemeral <- true - builder.Content <- msg - do! ctx.FollowUp(builder) |> Async.AwaitTask - } - - let sendFollowUpEmbed (ctx : IDiscordContext) embed = - async { - let builder = - DiscordFollowupMessageBuilder() - .AsEphemeral(true) - .AddEmbed(embed) - do! ctx.FollowUp(builder) |> Async.AwaitTask - } - - let sendFollowUpMessageWithButton (ctx : IDiscordContext) interactiveMessage = - async { - let builder = DiscordFollowupMessageBuilder() - let button = DiscordButtonComponent(ButtonStyle.Success, interactiveMessage.ButtonId, interactiveMessage.ButtonText) :> DiscordComponent - builder.AddComponents [| button |] |> ignore - builder.IsEphemeral <- true - builder.Content <- interactiveMessage.Message - do! ctx.FollowUp(builder) |> Async.AwaitTask - } - - let updateMessageWithGreyedOutButtons (ctx : IDiscordContext) interactiveMessage = - async { - let builder = DiscordInteractionResponseBuilder() - let button = DiscordButtonComponent(ButtonStyle.Success, interactiveMessage.ButtonId, interactiveMessage.ButtonText, true) :> DiscordComponent - builder.AddComponents [| button |] |> ignore - builder.IsEphemeral <- true - builder.Content <- interactiveMessage.Message - do! ctx.Respond(InteractionResponseType.UpdateMessage, builder) |> Async.AwaitTask - } - - let handleResultWithResponse ctx fn (player : Result) = - match player with - | Ok p -> fn p - | Error e -> async { do! sendFollowUpMessage ctx e } - From c45f1c6ca6aa2cb5ecd46e66da2b1445b185db33 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 26 Feb 2022 21:41:43 +0700 Subject: [PATCH 02/28] Move things around some more --- Bot/Bot.fsproj | 16 +- Bot/Game.fs | 337 --------------------------- Bot/GameHelpers.fs | 88 +++++++ Bot/GameTypes.fs | 117 ++++++++++ Bot/{ => Games}/HackerBattle.fs | 0 Bot/{ => Games}/RockPaperScissors.fs | 0 Bot/{ => Games}/SlotMachine.fs | 0 Bot/{ => Games}/Store.fs | 0 Bot/{ => Games}/Thief.fs | 0 Bot/{ => Games}/Trainer.fs | 0 Bot/Messaging.fs | 143 ++++++++++++ 11 files changed, 357 insertions(+), 344 deletions(-) delete mode 100644 Bot/Game.fs create mode 100644 Bot/GameHelpers.fs create mode 100644 Bot/GameTypes.fs rename Bot/{ => Games}/HackerBattle.fs (100%) rename Bot/{ => Games}/RockPaperScissors.fs (100%) rename Bot/{ => Games}/SlotMachine.fs (100%) rename Bot/{ => Games}/Store.fs (100%) rename Bot/{ => Games}/Thief.fs (100%) rename Bot/{ => Games}/Trainer.fs (100%) create mode 100644 Bot/Messaging.fs diff --git a/Bot/Bot.fsproj b/Bot/Bot.fsproj index 2def2ea..073b522 100644 --- a/Bot/Bot.fsproj +++ b/Bot/Bot.fsproj @@ -13,17 +13,19 @@ - + + + - - - - - - + + + + + + diff --git a/Bot/Game.fs b/Bot/Game.fs deleted file mode 100644 index 0b85933..0000000 --- a/Bot/Game.fs +++ /dev/null @@ -1,337 +0,0 @@ -namespace Degenz - -open System -open System.Threading.Tasks -open DSharpPlus -open DSharpPlus.Entities -open DSharpPlus.EventArgs -open DSharpPlus.SlashCommands -open Newtonsoft.Json - -[] -module Types = - - [] - type mins - - [] - type GBT - - type HackId = - | Virus = 0 - | RemoteAccess = 1 - | Worm = 2 - - type ShieldId = - | Firewall = 6 - | Encryption = 7 - | Cypher = 8 - - type ItemType = - | Hack = 0 - | Shield = 1 - | Food = 1 - - type ItemAttributes = { - Sell : bool - Buy : bool - Consume : bool - Drop : bool - } - with static member empty = { Sell = false ; Buy = false ; Consume = false ; Drop = false } - - type Item = { - Id : int - Name : string - Price : int - Type : ItemType - Power : int - Class : int - Cooldown : int - Attributes : ItemAttributes - } - with static member empty = - { Id = -1 - Name = "None" - Price = 0 - Type = ItemType.Hack - Power = 0 - Class = -1 - Cooldown = 0 - Attributes = ItemAttributes.empty } - - type HackResult = - | Strong - | Weak - - [] - type DiscordPlayer = { Id: uint64; Name: string } - with static member empty = { Id = 0uL ; Name = "None" } - - type HackEvent = { - IsInstigator : bool - Adversary : DiscordPlayer - Success : bool - HackId : int - } - - type PlayerEventType = - | Hacking of HackEvent - | Shielding of shieldId : int - | Stealing of instigator : bool * adversary : DiscordPlayer - | Imprison - - type PlayerEvent = - { Type : PlayerEventType - Cooldown : int - Timestamp : DateTime } - - type PlayerTraits = { - Strength : int - Focus : int - Luck : int - Charisma : int - } - with static member empty = { Strength = 0 ; Focus = 0 ; Luck = 0 ; Charisma = 0 } - - [] - type PlayerData = { - DiscordId : uint64 - Name : string - Inventory : Item array - Events : PlayerEvent array - Traits : PlayerTraits - Bank : int - } -// Achievements : string array -// XP : int - with member this.basicPlayer = { Id = this.DiscordId ; Name = this.Name } - static member empty = - { DiscordId = 0uL - Name = "None" - Inventory = [||] - Events = [||] - Traits = PlayerTraits.empty -// Achievements = [||] -// XP = 0 - Bank = 0 } - -module Armory = - let battleItems = - let file = System.IO.File.ReadAllText("Items.json") -// let file = System.IO.File.ReadAllText("Bot/Items.json") - JsonConvert.DeserializeObject(file) - - let hacks = battleItems |> Array.filter (fun bi -> match bi.Type with ItemType.Hack -> true | _ -> false) - let shields = battleItems |> Array.filter (fun bi -> match bi.Type with ItemType.Shield -> true | _ -> false) - - let getItem itemId = battleItems |> Array.find (fun w -> w.Id = itemId) - -module Messaging = - type InteractiveMessage = { - ButtonId : string - ButtonText : string - Message : string - } - - type DiscordContext = - | Interaction of InteractionContext - | Event of ComponentInteractionCreateEventArgs - - type IDiscordContext = - abstract member Respond : InteractionResponseType -> Task - abstract member Respond : InteractionResponseType * DiscordInteractionResponseBuilder -> Task - abstract member FollowUp : DiscordFollowupMessageBuilder -> Task - abstract member GetDiscordMember : unit -> DiscordMember - abstract member GetGuild : unit -> DiscordGuild - abstract member GetInteractionId : unit -> string - abstract member GetChannel : unit -> DiscordChannel - abstract member GetContext : unit -> obj - - type DiscordInteractionContext(ctx : InteractionContext) = - interface IDiscordContext with - member this.Respond responseType = - async { - do! ctx.Interaction.CreateResponseAsync(responseType) |> Async.AwaitTask - } |> Async.StartAsTask :> Task - member this.Respond (responseType, builder) = - async { - do! ctx.Interaction.CreateResponseAsync(responseType, builder) |> Async.AwaitTask - } |> Async.StartAsTask :> Task - member this.FollowUp(builder) = - async { - do! ctx.Interaction.CreateFollowupMessageAsync(builder) |> Async.AwaitTask |> Async.Ignore - } |> Async.StartAsTask :> Task - member this.GetDiscordMember() = ctx.Member - member this.GetGuild() = ctx.Guild - member this.GetInteractionId() = string ctx.InteractionId - member this.GetChannel() = ctx.Channel - member this.GetContext() = ctx - - type DiscordEventContext(ctx : ComponentInteractionCreateEventArgs) = - interface IDiscordContext with - member this.Respond responseType = - async { - do! ctx.Interaction.CreateResponseAsync(responseType) |> Async.AwaitTask - } |> Async.StartAsTask :> Task - member this.Respond (responseType, builder) = - async { - do! ctx.Interaction.CreateResponseAsync(responseType, builder) |> Async.AwaitTask - } |> Async.StartAsTask :> Task - member this.FollowUp(builder) = - async { - do! ctx.Interaction.CreateFollowupMessageAsync(builder) |> Async.AwaitTask |> Async.Ignore - } |> Async.StartAsTask :> Task - member this.GetDiscordMember() = ctx.User :?> DiscordMember - member this.GetGuild() = ctx.Guild - member this.GetInteractionId() = ctx.Id - member this.GetChannel() = ctx.Channel - member this.GetContext() = ctx - - 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 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 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 defer (ctx: IDiscordContext) = async { - let builder = DiscordInteractionResponseBuilder() - builder.IsEphemeral <- true - do! ctx.Respond(InteractionResponseType.DeferredChannelMessageWithSource, builder) |> Async.AwaitTask - } - - let sendSimpleResponse (ctx: IDiscordContext) msg = - async { - let builder = DiscordInteractionResponseBuilder() - builder.Content <- msg - builder.AsEphemeral true |> ignore - do! ctx.Respond(InteractionResponseType.ChannelMessageWithSource, builder) |> Async.AwaitTask - } - - let sendFollowUpMessage (ctx : IDiscordContext) msg = - async { - let builder = DiscordFollowupMessageBuilder() - builder.IsEphemeral <- true - builder.Content <- msg - do! ctx.FollowUp(builder) |> Async.AwaitTask - } - - let sendFollowUpEmbed (ctx : IDiscordContext) embed = - async { - let builder = - DiscordFollowupMessageBuilder() - .AsEphemeral(true) - .AddEmbed(embed) - do! ctx.FollowUp(builder) |> Async.AwaitTask - } - - let sendFollowUpMessageWithButton (ctx : IDiscordContext) interactiveMessage = - async { - let builder = DiscordFollowupMessageBuilder() - let button = DiscordButtonComponent(ButtonStyle.Success, interactiveMessage.ButtonId, interactiveMessage.ButtonText) :> DiscordComponent - builder.AddComponents [| button |] |> ignore - builder.IsEphemeral <- true - builder.Content <- interactiveMessage.Message - do! ctx.FollowUp(builder) |> Async.AwaitTask - } - - let updateMessageWithGreyedOutButtons (ctx : IDiscordContext) interactiveMessage = - async { - let builder = DiscordInteractionResponseBuilder() - let button = DiscordButtonComponent(ButtonStyle.Success, interactiveMessage.ButtonId, interactiveMessage.ButtonText, true) :> DiscordComponent - builder.AddComponents [| button |] |> ignore - builder.IsEphemeral <- true - builder.Content <- interactiveMessage.Message - do! ctx.Respond(InteractionResponseType.UpdateMessage, builder) |> Async.AwaitTask - } - - let handleResultWithResponse ctx fn (player : Result) = - match player with - | Ok p -> fn p - | Error e -> async { do! sendFollowUpMessage ctx e } - -open Messaging - -module Game = - let SameTargetAttackCooldown = System.TimeSpan.FromHours(1) - - let getClassButtonColor = function - | 0 -> ButtonStyle.Danger - | 1 -> ButtonStyle.Primary - | _ -> ButtonStyle.Success - - let getClassEmbedColor = function - | 0 -> DiscordColor.Red - | 1 -> DiscordColor.Blurple - | _ -> DiscordColor.Green - - let getGoodAgainst = function - | 0 -> ( ShieldId.Firewall , HackId.Virus ) - | 1 -> ( ShieldId.Encryption , HackId.RemoteAccess ) - | _ -> ( ShieldId.Cypher , HackId.Worm ) - -module Player = - let getItems itemType (player : PlayerData) = player.Inventory |> Array.filter (fun i -> i.Type = itemType) - let getHacks (player : PlayerData) = getItems ItemType.Hack player - let getShields (player : PlayerData) = getItems ItemType.Shield player - let getHackEvents player = - player.Events - |> Array.filter (fun act -> match act.Type with PlayerEventType.Hacking h -> h.IsInstigator | _ -> false) - let getShieldEvents player = - player.Events - |> Array.filter (fun act -> match act.Type with PlayerEventType.Shielding _ -> true | _ -> false) - - let removeExpiredActions player = - let actions = - player.Events - |> Array.filter (fun (act : PlayerEvent) -> - let cooldown = System.TimeSpan.FromMinutes(int act.Cooldown) - System.DateTime.UtcNow - act.Timestamp < cooldown) - { player with Events = actions } - - let modifyBank (player : PlayerData) amount = { player with Bank = max (player.Bank + amount) 0 } - -module Arsenal = - let battleItemFormat (items : Item array) = - match items with - | [||] -> "None" - | _ -> items |> Array.toList |> List.map (fun i -> i.Name) |> String.concat ", " - - let actionFormat (actions : PlayerEvent array) = - match actions with - | [||] -> "None" - | acts -> - acts - |> Array.map (fun act -> - match act.Type with - | Hacking h -> - let item = Armory.getItem h.HackId - let cooldown = Messaging.getTimeText false Game.SameTargetAttackCooldown act.Timestamp - $"Hacked {h.Adversary.Name} with {item.Name} {cooldown} ago" - | Shielding id -> - let item = Armory.getItem id - let cooldown = Messaging.getTimeText true (System.TimeSpan.FromMinutes(int act.Cooldown)) act.Timestamp - $"{item.Name} Shield active for {cooldown}" - | _ -> "") - |> Array.filter (System.String.IsNullOrWhiteSpace >> not) - |> String.concat "\n" - - let statusFormat p = - let hacks = Player.getHackEvents p - $"**Hacks:** {Player.getHacks p |> battleItemFormat}\n - **Shields:** {Player.getShields p |> battleItemFormat}\n - **Hack Attacks:**\n{ hacks |> Array.take (min hacks.Length 10) |> actionFormat}\n - **Active Shields:**\n{Player.getShieldEvents p |> actionFormat}" - diff --git a/Bot/GameHelpers.fs b/Bot/GameHelpers.fs new file mode 100644 index 0000000..3d494ea --- /dev/null +++ b/Bot/GameHelpers.fs @@ -0,0 +1,88 @@ +namespace Degenz + +open DSharpPlus +open DSharpPlus.Entities +open Newtonsoft.Json + +module Armory = + let battleItems = + let file = System.IO.File.ReadAllText("Items.json") +// let file = System.IO.File.ReadAllText("Bot/Items.json") + JsonConvert.DeserializeObject(file) + + let hacks = battleItems |> Array.filter (fun bi -> match bi.Type with ItemType.Hack -> true | _ -> false) + let shields = battleItems |> Array.filter (fun bi -> match bi.Type with ItemType.Shield -> true | _ -> false) + + let getItem itemId = battleItems |> Array.find (fun w -> w.Id = itemId) + +module WeaponClass = + let SameTargetAttackCooldown = System.TimeSpan.FromHours(1) + + let getClassButtonColor = function + | 0 -> ButtonStyle.Danger + | 1 -> ButtonStyle.Primary + | _ -> ButtonStyle.Success + + let getClassEmbedColor = function + | 0 -> DiscordColor.Red + | 1 -> DiscordColor.Blurple + | _ -> DiscordColor.Green + + let getGoodAgainst = function + | 0 -> ( ShieldId.Firewall , HackId.Virus ) + | 1 -> ( ShieldId.Encryption , HackId.RemoteAccess ) + | _ -> ( ShieldId.Cypher , HackId.Worm ) + +module Player = + let getItems itemType (player : PlayerData) = player.Inventory |> Array.filter (fun i -> i.Type = itemType) + let getHacks (player : PlayerData) = getItems ItemType.Hack player + let getShields (player : PlayerData) = getItems ItemType.Shield player + let getHackEvents player = + player.Events + |> Array.filter (fun act -> match act.Type with PlayerEventType.Hacking h -> h.IsInstigator | _ -> false) + let getShieldEvents player = + player.Events + |> Array.filter (fun act -> match act.Type with PlayerEventType.Shielding _ -> true | _ -> false) + + let removeExpiredActions player = + let actions = + player.Events + |> Array.filter (fun (act : PlayerEvent) -> + let cooldown = System.TimeSpan.FromMinutes(int act.Cooldown) + System.DateTime.UtcNow - act.Timestamp < cooldown) + { player with Events = actions } + + let modifyBank (player : PlayerData) amount = { player with Bank = max (player.Bank + amount) 0 } + +module Arsenal = + let battleItemFormat (items : Item array) = + match items with + | [||] -> "None" + | _ -> items |> Array.toList |> List.map (fun i -> i.Name) |> String.concat ", " + + let actionFormat (actions : PlayerEvent array) = + match actions with + | [||] -> "None" + | acts -> + acts + |> Array.map (fun act -> + match act.Type with + | Hacking h -> + let item = Armory.getItem h.HackId + let cooldown = Messaging.getTimeText false WeaponClass.SameTargetAttackCooldown act.Timestamp + $"Hacked {h.Adversary.Name} with {item.Name} {cooldown} ago" + | Shielding id -> + let item = Armory.getItem id + let cooldown = Messaging.getTimeText true (System.TimeSpan.FromMinutes(int act.Cooldown)) act.Timestamp + $"{item.Name} Shield active for {cooldown}" + | _ -> "") + |> Array.filter (System.String.IsNullOrWhiteSpace >> not) + |> String.concat "\n" + + let statusFormat p = + let hacks = Player.getHackEvents p + $"**Hacks:** {Player.getHacks p |> battleItemFormat}\n + **Shields:** {Player.getShields p |> battleItemFormat}\n + **Hack Attacks:**\n{ hacks |> Array.take (min hacks.Length 10) |> actionFormat}\n + **Active Shields:**\n{Player.getShieldEvents p |> actionFormat}" + diff --git a/Bot/GameTypes.fs b/Bot/GameTypes.fs new file mode 100644 index 0000000..6942873 --- /dev/null +++ b/Bot/GameTypes.fs @@ -0,0 +1,117 @@ +namespace Degenz + +open System +open System.Threading.Tasks +open DSharpPlus +open DSharpPlus.Entities +open DSharpPlus.EventArgs +open DSharpPlus.SlashCommands +open Newtonsoft.Json + +[] +module Types = + + [] + type mins + + [] + type GBT + + type HackId = + | Virus = 0 + | RemoteAccess = 1 + | Worm = 2 + + type ShieldId = + | Firewall = 6 + | Encryption = 7 + | Cypher = 8 + + type ItemType = + | Hack = 0 + | Shield = 1 + | Food = 1 + + type ItemAttributes = { + Sell : bool + Buy : bool + Consume : bool + Drop : bool + } + with static member empty = { Sell = false ; Buy = false ; Consume = false ; Drop = false } + + type Item = { + Id : int + Name : string + Price : int + Type : ItemType + Power : int + Class : int + Cooldown : int + Attributes : ItemAttributes + } + with static member empty = + { Id = -1 + Name = "None" + Price = 0 + Type = ItemType.Hack + Power = 0 + Class = -1 + Cooldown = 0 + Attributes = ItemAttributes.empty } + + type HackResult = + | Strong + | Weak + + [] + type DiscordPlayer = { Id: uint64; Name: string } + with static member empty = { Id = 0uL ; Name = "None" } + + type HackEvent = { + IsInstigator : bool + Adversary : DiscordPlayer + Success : bool + HackId : int + } + + type PlayerEventType = + | Hacking of HackEvent + | Shielding of shieldId : int + | Stealing of instigator : bool * adversary : DiscordPlayer + | Imprison + + type PlayerEvent = + { Type : PlayerEventType + Cooldown : int + Timestamp : DateTime } + + type PlayerTraits = { + Strength : int + Focus : int + Luck : int + Charisma : int + } + with static member empty = { Strength = 0 ; Focus = 0 ; Luck = 0 ; Charisma = 0 } + + [] + type PlayerData = { + DiscordId : uint64 + Name : string + Inventory : Item array + Events : PlayerEvent array + Traits : PlayerTraits + Bank : int + } +// Achievements : string array +// XP : int + with member this.basicPlayer = { Id = this.DiscordId ; Name = this.Name } + static member empty = + { DiscordId = 0uL + Name = "None" + Inventory = [||] + Events = [||] + Traits = PlayerTraits.empty +// Achievements = [||] +// XP = 0 + Bank = 0 } diff --git a/Bot/HackerBattle.fs b/Bot/Games/HackerBattle.fs similarity index 100% rename from Bot/HackerBattle.fs rename to Bot/Games/HackerBattle.fs diff --git a/Bot/RockPaperScissors.fs b/Bot/Games/RockPaperScissors.fs similarity index 100% rename from Bot/RockPaperScissors.fs rename to Bot/Games/RockPaperScissors.fs diff --git a/Bot/SlotMachine.fs b/Bot/Games/SlotMachine.fs similarity index 100% rename from Bot/SlotMachine.fs rename to Bot/Games/SlotMachine.fs diff --git a/Bot/Store.fs b/Bot/Games/Store.fs similarity index 100% rename from Bot/Store.fs rename to Bot/Games/Store.fs diff --git a/Bot/Thief.fs b/Bot/Games/Thief.fs similarity index 100% rename from Bot/Thief.fs rename to Bot/Games/Thief.fs diff --git a/Bot/Trainer.fs b/Bot/Games/Trainer.fs similarity index 100% rename from Bot/Trainer.fs rename to Bot/Games/Trainer.fs diff --git a/Bot/Messaging.fs b/Bot/Messaging.fs new file mode 100644 index 0000000..9a0bfd9 --- /dev/null +++ b/Bot/Messaging.fs @@ -0,0 +1,143 @@ +module Degenz.Messaging + +open System +open System.Threading.Tasks +open DSharpPlus +open DSharpPlus.Entities +open DSharpPlus.EventArgs +open DSharpPlus.SlashCommands + +type InteractiveMessage = { + ButtonId : string + ButtonText : string + Message : string +} + +type DiscordContext = + | Interaction of InteractionContext + | Event of ComponentInteractionCreateEventArgs + +type IDiscordContext = + abstract member Respond : InteractionResponseType -> Task + abstract member Respond : InteractionResponseType * DiscordInteractionResponseBuilder -> Task + abstract member FollowUp : DiscordFollowupMessageBuilder -> Task + abstract member GetDiscordMember : unit -> DiscordMember + abstract member GetGuild : unit -> DiscordGuild + abstract member GetInteractionId : unit -> string + abstract member GetChannel : unit -> DiscordChannel + abstract member GetContext : unit -> obj + +type DiscordInteractionContext(ctx : InteractionContext) = + interface IDiscordContext with + member this.Respond responseType = + async { + do! ctx.Interaction.CreateResponseAsync(responseType) |> Async.AwaitTask + } |> Async.StartAsTask :> Task + member this.Respond (responseType, builder) = + async { + do! ctx.Interaction.CreateResponseAsync(responseType, builder) |> Async.AwaitTask + } |> Async.StartAsTask :> Task + member this.FollowUp(builder) = + async { + do! ctx.Interaction.CreateFollowupMessageAsync(builder) |> Async.AwaitTask |> Async.Ignore + } |> Async.StartAsTask :> Task + member this.GetDiscordMember() = ctx.Member + member this.GetGuild() = ctx.Guild + member this.GetInteractionId() = string ctx.InteractionId + member this.GetChannel() = ctx.Channel + member this.GetContext() = ctx + +type DiscordEventContext(ctx : ComponentInteractionCreateEventArgs) = + interface IDiscordContext with + member this.Respond responseType = + async { + do! ctx.Interaction.CreateResponseAsync(responseType) |> Async.AwaitTask + } |> Async.StartAsTask :> Task + member this.Respond (responseType, builder) = + async { + do! ctx.Interaction.CreateResponseAsync(responseType, builder) |> Async.AwaitTask + } |> Async.StartAsTask :> Task + member this.FollowUp(builder) = + async { + do! ctx.Interaction.CreateFollowupMessageAsync(builder) |> Async.AwaitTask |> Async.Ignore + } |> Async.StartAsTask :> Task + member this.GetDiscordMember() = ctx.User :?> DiscordMember + member this.GetGuild() = ctx.Guild + member this.GetInteractionId() = ctx.Id + member this.GetChannel() = ctx.Channel + member this.GetContext() = ctx + +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 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 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 defer (ctx: IDiscordContext) = async { + let builder = DiscordInteractionResponseBuilder() + builder.IsEphemeral <- true + do! ctx.Respond(InteractionResponseType.DeferredChannelMessageWithSource, builder) |> Async.AwaitTask +} + +let sendSimpleResponse (ctx: IDiscordContext) msg = + async { + let builder = DiscordInteractionResponseBuilder() + builder.Content <- msg + builder.AsEphemeral true |> ignore + do! ctx.Respond(InteractionResponseType.ChannelMessageWithSource, builder) |> Async.AwaitTask + } + +let sendFollowUpMessage (ctx : IDiscordContext) msg = + async { + let builder = DiscordFollowupMessageBuilder() + builder.IsEphemeral <- true + builder.Content <- msg + do! ctx.FollowUp(builder) |> Async.AwaitTask + } + +let sendFollowUpEmbed (ctx : IDiscordContext) embed = + async { + let builder = + DiscordFollowupMessageBuilder() + .AsEphemeral(true) + .AddEmbed(embed) + do! ctx.FollowUp(builder) |> Async.AwaitTask + } + +let sendFollowUpMessageWithButton (ctx : IDiscordContext) interactiveMessage = + async { + let builder = DiscordFollowupMessageBuilder() + let button = DiscordButtonComponent(ButtonStyle.Success, interactiveMessage.ButtonId, interactiveMessage.ButtonText) :> DiscordComponent + builder.AddComponents [| button |] |> ignore + builder.IsEphemeral <- true + builder.Content <- interactiveMessage.Message + do! ctx.FollowUp(builder) |> Async.AwaitTask + } + +let updateMessageWithGreyedOutButtons (ctx : IDiscordContext) interactiveMessage = + async { + let builder = DiscordInteractionResponseBuilder() + let button = DiscordButtonComponent(ButtonStyle.Success, interactiveMessage.ButtonId, interactiveMessage.ButtonText, true) :> DiscordComponent + builder.AddComponents [| button |] |> ignore + builder.IsEphemeral <- true + builder.Content <- interactiveMessage.Message + do! ctx.Respond(InteractionResponseType.UpdateMessage, builder) |> Async.AwaitTask + } + +let handleResultWithResponse ctx fn (player : Result) = + match player with + | Ok p -> fn p + | Error e -> async { do! sendFollowUpMessage ctx e } + From 9659656e4a47ed6c42db4201d370cd353c9451dc Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 26 Feb 2022 21:50:14 +0700 Subject: [PATCH 03/28] Reorder things, and remove explicit modules, remove unused opens --- Bot/Bot.fsproj | 4 ++-- Bot/DbService.fs | 4 +--- Bot/Embeds.fs | 17 ++++++++--------- Bot/GameHelpers.fs | 1 + Bot/GameTypes.fs | 6 ------ Bot/Games/HackerBattle.fs | 13 +++++++------ Bot/Games/RockPaperScissors.fs | 1 - Bot/Games/SlotMachine.fs | 1 - Bot/Games/Store.fs | 9 +++++---- Bot/Games/Thief.fs | 5 +++-- Bot/Games/Trainer.fs | 1 - Bot/Messaging.fs | 6 ------ Bot/PlayerInteractions.fs | 5 +++++ 13 files changed, 32 insertions(+), 41 deletions(-) diff --git a/Bot/Bot.fsproj b/Bot/Bot.fsproj index 073b522..d7929c4 100644 --- a/Bot/Bot.fsproj +++ b/Bot/Bot.fsproj @@ -13,11 +13,11 @@ - + + - diff --git a/Bot/DbService.fs b/Bot/DbService.fs index de6f76d..45e60db 100644 --- a/Bot/DbService.fs +++ b/Bot/DbService.fs @@ -1,8 +1,6 @@ module Degenz.DbService -open System.Security.Cryptography.X509Certificates -open Degenz.Types - +open Degenz open System open Npgsql.FSharp diff --git a/Bot/Embeds.fs b/Bot/Embeds.fs index be37bb7..c3d7097 100644 --- a/Bot/Embeds.fs +++ b/Bot/Embeds.fs @@ -1,7 +1,6 @@ module Degenz.Embeds open System -open DSharpPlus open Degenz.Messaging open Degenz.Types open DSharpPlus.Entities @@ -45,7 +44,7 @@ let constructButtons (actionId: string) (buttonInfo : string) (player: PlayerDat | Hacking h -> h.HackId = item.Id && h.IsInstigator | Shielding id -> id = item.Id | _ -> false) - let btnColor = Game.getClassButtonColor item.Class + let btnColor = WeaponClass.getClassButtonColor item.Class match action , ignoreCooldown with | None , _ | Some _ , true -> DiscordButtonComponent(btnColor, $"{actionId}-{item.Id}-{buttonInfo}-{player.Name}", $"{item.Name}") @@ -64,7 +63,7 @@ let pickDefense actionId player isTrainer = for s in Player.getShields player |> Array.sortBy (fun i -> i.Power) do let hours = TimeSpan.FromMinutes(int s.Cooldown).TotalHours - let against = Game.getGoodAgainst(s.Class) |> snd + let against = WeaponClass.getGoodAgainst(s.Class) |> snd embed.AddField(s.Name, $"Active {hours} hours\nDefeats {against}", true) |> ignore DiscordFollowupMessageBuilder() @@ -132,19 +131,19 @@ let getBuyItemsEmbed (player : PlayerData) (itemType : ItemType) (store : Item a | _ -> embed // .AddField($"Defensive Strength |", string item.Power, true) - .AddField($"Strong against |", Game.getGoodAgainst item.Class |> snd |> string, true) + .AddField($"Strong against |", WeaponClass.getGoodAgainst item.Class |> snd |> string, true) .AddField("Active For |", $"{TimeSpan.FromMinutes(int item.Cooldown).Hours} hours", true) .WithThumbnail(getShieldIcon (enum(item.Id))) |> ignore embed .AddField("Price 💰", (if item.Price = 0 then "Free" else $"{item.Price} $GBT"), true) - .WithColor(Game.getClassEmbedColor item.Class) + .WithColor(WeaponClass.getClassEmbedColor item.Class) .WithTitle($"{item.Name}") |> ignore let button = if player.Inventory |> Array.exists (fun i -> i.Id = item.Id) - then DiscordButtonComponent(Game.getClassButtonColor item.Class, $"Buy-{item.Id}", $"Own {item.Name}", true) - else DiscordButtonComponent(Game.getClassButtonColor item.Class, $"Buy-{item.Id}", $"Buy {item.Name}") + then DiscordButtonComponent(WeaponClass.getClassButtonColor item.Class, $"Buy-{item.Id}", $"Own {item.Name}", true) + else DiscordButtonComponent(WeaponClass.getClassButtonColor item.Class, $"Buy-{item.Id}", $"Buy {item.Name}") embed.Build() , button :> DiscordComponent) |> Array.unzip @@ -165,9 +164,9 @@ let getSellEmbed (itemType : ItemType) (player : PlayerData) = embed .AddField("Sell For 💰", $"{item.Price} $GBT", true) .WithTitle($"{item.Name}") - .WithColor(Game.getClassEmbedColor item.Class) + .WithColor(WeaponClass.getClassEmbedColor item.Class) |> ignore - let button = DiscordButtonComponent(Game.getClassButtonColor item.Class, $"Sell-{item.Id}", $"Sell {item.Name}") + let button = DiscordButtonComponent(WeaponClass.getClassButtonColor item.Class, $"Sell-{item.Id}", $"Sell {item.Name}") embed.Build() , button :> DiscordComponent) |> Array.unzip diff --git a/Bot/GameHelpers.fs b/Bot/GameHelpers.fs index 3d494ea..0bac34a 100644 --- a/Bot/GameHelpers.fs +++ b/Bot/GameHelpers.fs @@ -16,6 +16,7 @@ module Armory = let getItem itemId = battleItems |> Array.find (fun w -> w.Id = itemId) module WeaponClass = + // TODO: Find a different place to put this let SameTargetAttackCooldown = System.TimeSpan.FromHours(1) let getClassButtonColor = function diff --git a/Bot/GameTypes.fs b/Bot/GameTypes.fs index 6942873..0151b3d 100644 --- a/Bot/GameTypes.fs +++ b/Bot/GameTypes.fs @@ -1,12 +1,6 @@ namespace Degenz open System -open System.Threading.Tasks -open DSharpPlus -open DSharpPlus.Entities -open DSharpPlus.EventArgs -open DSharpPlus.SlashCommands -open Newtonsoft.Json [] module Types = diff --git a/Bot/Games/HackerBattle.fs b/Bot/Games/HackerBattle.fs index f58dd28..b710e28 100644 --- a/Bot/Games/HackerBattle.fs +++ b/Bot/Games/HackerBattle.fs @@ -8,6 +8,7 @@ open DSharpPlus.EventArgs open DSharpPlus.SlashCommands open Degenz open Degenz.Messaging +open Degenz.PlayerInteractions let checkPlayerIsAttackingThemselves defender attacker = match attacker.DiscordId = defender.DiscordId with @@ -95,7 +96,7 @@ let updateCombatants successfulHack (attacker : PlayerData) (defender : PlayerDa } { Type = Hacking hackEvent Timestamp = DateTime.UtcNow - Cooldown = if isDefenderEvent then int Game.SameTargetAttackCooldown.TotalMinutes * 1 else hack.Cooldown } + Cooldown = if isDefenderEvent then int WeaponClass.SameTargetAttackCooldown.TotalMinutes * 1 else hack.Cooldown } [ DbService.updatePlayer GuildEnvironment.pgDb <| updatePlayer prize (event false) attacker DbService.updatePlayer GuildEnvironment.pgDb <| updatePlayer -prize (event true) defender @@ -136,7 +137,7 @@ let failedHack (ctx : IDiscordContext) attacker defender hack = } let hack (target : DiscordUser) (ctx : IDiscordContext) = - PlayerInteractions.executePlayerActionWithTarget target ctx (fun attacker defender -> async { + executePlayerActionWithTarget target ctx (fun attacker defender -> async { do! attacker |> Player.removeExpiredActions |> checkAlreadyHackedTarget defender @@ -151,7 +152,7 @@ let hack (target : DiscordUser) (ctx : IDiscordContext) = }) let handleAttack (ctx : IDiscordContext) = - PlayerInteractions.executePlayerAction ctx (fun attacker -> async { + executePlayerAction ctx (fun attacker -> async { let tokens = ctx.GetInteractionId().Split("-") let hackId = int tokens.[1] let hack = Armory.getItem hackId @@ -176,7 +177,7 @@ let handleAttack (ctx : IDiscordContext) = }) let defend (ctx : IDiscordContext) = - PlayerInteractions.executePlayerAction ctx (fun player -> async { + executePlayerAction ctx (fun player -> async { if Player.getShields player |> Array.length > 0 then let p = Player.removeExpiredActions player let embed = Embeds.pickDefense "Defend" p false @@ -187,7 +188,7 @@ let defend (ctx : IDiscordContext) = }) let handleDefense (ctx : IDiscordContext) = - PlayerInteractions.executePlayerAction ctx (fun player -> async { + executePlayerAction ctx (fun player -> async { let tokens = ctx.GetInteractionId().Split("-") let shieldId = int tokens.[1] let shield = Armory.getItem shieldId @@ -218,7 +219,7 @@ let handleDefense (ctx : IDiscordContext) = }) let arsenal (ctx : IDiscordContext) = - PlayerInteractions.executePlayerAction ctx (fun player -> async { + executePlayerAction ctx (fun player -> async { let updatedPlayer = Player.removeExpiredActions player let builder = DiscordFollowupMessageBuilder() let embed = DiscordEmbedBuilder() diff --git a/Bot/Games/RockPaperScissors.fs b/Bot/Games/RockPaperScissors.fs index 7b93835..8183495 100644 --- a/Bot/Games/RockPaperScissors.fs +++ b/Bot/Games/RockPaperScissors.fs @@ -1,6 +1,5 @@ module Degenz.RockPaperScissors -open System open System.Threading.Tasks open DSharpPlus open DSharpPlus.Entities diff --git a/Bot/Games/SlotMachine.fs b/Bot/Games/SlotMachine.fs index b401381..62f54a3 100644 --- a/Bot/Games/SlotMachine.fs +++ b/Bot/Games/SlotMachine.fs @@ -1,7 +1,6 @@ module Degenz.SlotMachine open System -open System.Threading.Tasks open DSharpPlus open DSharpPlus.Entities open DSharpPlus.SlashCommands diff --git a/Bot/Games/Store.fs b/Bot/Games/Store.fs index 3e2efb4..c105a40 100644 --- a/Bot/Games/Store.fs +++ b/Bot/Games/Store.fs @@ -7,6 +7,7 @@ open DSharpPlus.EventArgs open DSharpPlus.SlashCommands open Degenz open Degenz.Messaging +open Degenz.PlayerInteractions let checkHasSufficientFunds (item : Item) player = if player.Bank - item.Price >= 0 @@ -29,13 +30,13 @@ let checkHasItemsInArsenal itemType player = else Error $"You currently have no {itemType}s in your arsenal to sell!" let buy itemType (ctx : IDiscordContext) = - PlayerInteractions.executePlayerAction ctx (fun player -> async { + executePlayerAction ctx (fun player -> async { let itemStore = Embeds.getBuyItemsEmbed player itemType Armory.battleItems do! ctx.FollowUp itemStore |> Async.AwaitTask }) let sell itemType (ctx : IDiscordContext) = - PlayerInteractions.executePlayerAction ctx (fun player -> async { + executePlayerAction ctx (fun player -> async { match checkHasItemsInArsenal itemType player with | Ok _ -> let itemStore = Embeds.getSellEmbed itemType player do! ctx.FollowUp(itemStore) |> Async.AwaitTask @@ -44,7 +45,7 @@ let sell itemType (ctx : IDiscordContext) = // TODO: When you buy a shield, prompt the user to activate it let handleBuyItem (ctx : IDiscordContext) itemId = - PlayerInteractions.executePlayerAction ctx (fun player -> async { + executePlayerAction ctx (fun player -> async { let item = Armory.battleItems |> Array.find (fun w -> w.Id = itemId) do! player |> checkHasSufficientFunds item @@ -58,7 +59,7 @@ let handleBuyItem (ctx : IDiscordContext) itemId = }) let handleSell (ctx : IDiscordContext) itemId = - PlayerInteractions.executePlayerAction ctx (fun player -> async { + executePlayerAction ctx (fun player -> async { let item = Armory.getItem itemId do! player diff --git a/Bot/Games/Thief.fs b/Bot/Games/Thief.fs index 5cdb0fa..fd1a196 100644 --- a/Bot/Games/Thief.fs +++ b/Bot/Games/Thief.fs @@ -7,6 +7,7 @@ open DSharpPlus.Entities open DSharpPlus.EventArgs open DSharpPlus.SlashCommands open Degenz.Messaging +open Degenz.PlayerInteractions let ThiefCooldown = TimeSpan.FromMinutes(1) let VictimRecovery = TimeSpan.FromHours(1) @@ -119,7 +120,7 @@ let calculateWinPercentage amountRequested bank attackerStrength defenderStrengt //calculateWinPercentage 50 200 100 85 let steal target amount (ctx : IDiscordContext) = - PlayerInteractions.executePlayerActionWithTarget target ctx (fun thief victim -> async { do! + executePlayerActionWithTarget target ctx (fun thief victim -> async { do! thief |> checkPlayerIsAttackingThemselves victim // |> checkVictimStealingCooldown victim @@ -219,7 +220,7 @@ let handleSteal (ctx : IDiscordContext) = } if answer = "yes" then let targetId = uint64 tokens.[2] - PlayerInteractions.executePlayerActionWithTargetId true targetId ctx (fun attacker defender -> async { + executePlayerActionWithTargetId true targetId ctx (fun attacker defender -> async { do! attacker |> Player.removeExpiredActions // |> checkVictimStealingCooldown defender diff --git a/Bot/Games/Trainer.fs b/Bot/Games/Trainer.fs index 781bf57..e8958e9 100644 --- a/Bot/Games/Trainer.fs +++ b/Bot/Games/Trainer.fs @@ -4,7 +4,6 @@ open System.Text open System.Threading.Tasks open DSharpPlus open DSharpPlus.Entities -open DSharpPlus.EventArgs open Degenz.Types open Degenz.Messaging diff --git a/Bot/Messaging.fs b/Bot/Messaging.fs index 9a0bfd9..d8298f1 100644 --- a/Bot/Messaging.fs +++ b/Bot/Messaging.fs @@ -135,9 +135,3 @@ let updateMessageWithGreyedOutButtons (ctx : IDiscordContext) interactiveMessage builder.Content <- interactiveMessage.Message do! ctx.Respond(InteractionResponseType.UpdateMessage, builder) |> Async.AwaitTask } - -let handleResultWithResponse ctx fn (player : Result) = - match player with - | Ok p -> fn p - | Error e -> async { do! sendFollowUpMessage ctx e } - diff --git a/Bot/PlayerInteractions.fs b/Bot/PlayerInteractions.fs index 3122c8c..bfe188e 100644 --- a/Bot/PlayerInteractions.fs +++ b/Bot/PlayerInteractions.fs @@ -53,6 +53,11 @@ let executePlayerActionWithTargetId defer (targetId : uint64) (ctx : IDiscordCon | _ , None -> do! Messaging.sendFollowUpMessage ctx "Your target is not connected to the network, they must join first by using the /redpill command" } |> Async.StartAsTask :> Task +let handleResultWithResponse ctx fn (player : Result) = + match player with + | Ok p -> fn p + | Error e -> async { do! Messaging.sendFollowUpMessage ctx e } + module Commands = let newPlayer nickname (membr : uint64) = let rand = System.Random(System.Guid.NewGuid().GetHashCode()) From a0267b4cbbf714f5ec0131ffb5c13854e7cc5abd Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sun, 27 Feb 2022 15:12:30 +0700 Subject: [PATCH 04/28] New item types --- Bot/Bot.fs | 15 +-- Bot/DbService.fs | 14 +-- Bot/Embeds.fs | 138 +++++++++++++--------------- Bot/GameHelpers.fs | 95 ++++++++++++++----- Bot/GameTypes.fs | 182 ++++++++++++++++++------------------- Bot/Games/HackerBattle.fs | 61 +++++++------ Bot/Games/Store.fs | 42 ++++----- Bot/Games/Thief.fs | 10 +- Bot/Games/Trainer.fs | 37 ++++---- Bot/Items.json | 93 ++++++++----------- Bot/PlayerInteractions.fs | 50 ---------- Bot/XP.fs | 2 +- DbService/DbService.fsproj | 11 --- DbService/paket.references | 8 -- Shared/Shared.fs | 9 -- Shared/Shared.fsproj | 12 --- Shared/paket.references | 3 - 17 files changed, 353 insertions(+), 429 deletions(-) delete mode 100644 DbService/DbService.fsproj delete mode 100644 DbService/paket.references delete mode 100644 Shared/Shared.fs delete mode 100644 Shared/Shared.fsproj delete mode 100644 Shared/paket.references diff --git a/Bot/Bot.fs b/Bot/Bot.fs index f58fcd8..93ae670 100644 --- a/Bot/Bot.fs +++ b/Bot/Bot.fs @@ -19,6 +19,7 @@ let storeConfig = DiscordConfiguration() let stealConfig = DiscordConfiguration() //let slotMachineConfig = DiscordConfiguration() //hackerBattleConfig.MinimumLogLevel <- Microsoft.Extensions.Logging.LogLevel.Trace +//storeConfig.MinimumLogLevel <- Microsoft.Extensions.Logging.LogLevel.Trace //let configs = [| hackerBattleConfig ; storeConfig ; slotMachineConfig ; |] let configs = [ hackerBattleConfig ; storeConfig ; stealConfig ] @@ -99,20 +100,6 @@ stealBot.ConnectAsync() |> Async.AwaitTask |> Async.RunSynchronously // 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 - - Task.Delay(-1) |> Async.AwaitTask |> Async.RunSynchronously diff --git a/Bot/DbService.fs b/Bot/DbService.fs index 45e60db..081b8d5 100644 --- a/Bot/DbService.fs +++ b/Bot/DbService.fs @@ -1,23 +1,23 @@ module Degenz.DbService -open Degenz open System open Npgsql.FSharp +open Degenz type User = { Name : string DiscordId : uint64 Bank : int Strength : int - Inventory : int array + Inventory : int list } let mapBack user : PlayerData = { DiscordId = user.DiscordId Name = user.Name - Inventory = user.Inventory |> Array.choose (fun id -> Armory.battleItems |> Array.tryFind (fun i -> i.Id = id)) + Inventory = user.Inventory |> List.choose (fun id -> Armory.battleItems |> List.tryFind (fun item -> item.Id = id)) Events = [||] - Traits = { PlayerTraits.empty with Strength = user.Strength } + Stats = [ { Id = StatId.Strength ; Amount = user.Strength } ] Bank = user.Bank } @@ -70,7 +70,7 @@ let tryFindPlayer connStr (discordId : uint64) = Name = read.string "display_name" Bank = read.int "gbt" * 1 Strength = read.int "strength" - Inventory = read.intArray "inventory" + Inventory = read.intArray "inventory" |> Array.toList }) |> Async.AwaitTask match List.tryHead user with @@ -90,8 +90,8 @@ let updatePlayer connStr (player : PlayerData) = |> Sql.parameters [ "did", Sql.string (string player.DiscordId) "gbt", Sql.int (int player.Bank) - "str", Sql.int (int player.Traits.Strength) - "inv", Sql.intArray (player.Inventory |> Array.map (fun i -> i.Id)) + "str", Sql.int (player |> Player.getStat StatId.Strength |> int) + "inv", Sql.intArray (player.Inventory |> Array.ofList |> Array.map (fun item -> item.Id)) ] |> Sql.query """ UPDATE "user" SET gbt = @gbt, strength = @str, inventory = @inv diff --git a/Bot/Embeds.fs b/Bot/Embeds.fs index c3d7097..9fd59a4 100644 --- a/Bot/Embeds.fs +++ b/Bot/Embeds.fs @@ -8,35 +8,29 @@ open DSharpPlus.Entities 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 getHackIcon = function - | 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" +let getItemIcon id = + match enum(id) with + | ItemId.Virus -> "https://s10.gifyu.com/images/Virus-icon.jpg" + | ItemId.RemoteAccess -> "https://s10.gifyu.com/images/Mind-Control-Degenz-V2-min.jpg" + | ItemId.Worm -> "https://s10.gifyu.com/images/WormBugAttack_Degenz-min.jpg" + | ItemId.Firewall -> "https://s10.gifyu.com/images/Defense-GIF-1-Degenz-1.jpg" + | ItemId.Encryption -> "https://s10.gifyu.com/images/Encryption-Degenz-V2-1-min.jpg" + | ItemId.Cypher -> "https://s10.gifyu.com/images/Cypher-Smaller.jpg" | _ -> hackGif -let getShieldIcon = function - | 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 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" +let getItemGif id = + match enum(id) with + | ItemId.Virus -> "https://s10.gifyu.com/images/Attack-DegenZ-1.gif" + | ItemId.RemoteAccess -> "https://s10.gifyu.com/images/Mind-Control-Degenz-V2-min.gif" + | ItemId.Worm -> "https://s10.gifyu.com/images/WormBugAttack_Degenz-min.gif" + | ItemId.Firewall -> "https://s10.gifyu.com/images/Defense-GIF-1-Degenz-min.gif" + | ItemId.Encryption -> "https://s10.gifyu.com/images/Encryption-Degenz-V2-1-min.gif" + | ItemId.Cypher -> "https://s10.gifyu.com/images/Cypher-Smaller.gif" | _ -> 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" - | _ -> shieldGif - -let constructButtons (actionId: string) (buttonInfo : string) (player: PlayerData) itemType ignoreCooldown = - player - |> Player.getItems itemType - |> Array.sortBy (fun i -> i.Power) - |> Array.map (fun item -> +let constructButtons (actionId: string) (buttonInfo : string) (player: PlayerData) items ignoreCooldown = + items + |> List.map (fun item -> let action = player.Events |> Array.tryFind (fun i -> @@ -44,7 +38,7 @@ let constructButtons (actionId: string) (buttonInfo : string) (player: PlayerDat | Hacking h -> h.HackId = item.Id && h.IsInstigator | Shielding id -> id = item.Id | _ -> false) - let btnColor = WeaponClass.getClassButtonColor item.Class + let btnColor = WeaponClass.getClassButtonColor item match action , ignoreCooldown with | None , _ | Some _ , true -> DiscordButtonComponent(btnColor, $"{actionId}-{item.Id}-{buttonInfo}-{player.Name}", $"{item.Name}") @@ -54,17 +48,18 @@ let constructButtons (actionId: string) (buttonInfo : string) (player: PlayerDat |> Seq.cast let pickDefense actionId player isTrainer = - let buttons = constructButtons actionId (string player.DiscordId) player ItemType.Shield isTrainer + let shieldItems = player |> Player.getShieldItems + let buttons = constructButtons actionId (string player.DiscordId) player shieldItems isTrainer let embed = DiscordEmbedBuilder() .WithTitle("Shield Defense") .WithDescription("Pick a shield to protect yourself from hacks") - for s in Player.getShields player |> Array.sortBy (fun i -> i.Power) do - let hours = TimeSpan.FromMinutes(int s.Cooldown).TotalHours - let against = WeaponClass.getGoodAgainst(s.Class) |> snd - embed.AddField(s.Name, $"Active {hours} hours\nDefeats {against}", true) |> ignore + for (item,sClass,cooldown) in Player.getShields player do + let hours = TimeSpan.FromMinutes(int cooldown).TotalHours + let against = WeaponClass.getGoodAgainst(sClass) |> snd + embed.AddField(item.Name, $"Active {hours} hours\nDefeats {against}", true) |> ignore DiscordFollowupMessageBuilder() .AddComponents(buttons) @@ -72,7 +67,8 @@ let pickDefense actionId player isTrainer = .AsEphemeral(true) let pickHack actionId attacker defender isTrainer = - let buttons = constructButtons actionId $"{defender.DiscordId}-{defender.Name}" attacker ItemType.Hack isTrainer + let hackItems = attacker |> Player.getHackItems + let buttons = constructButtons actionId $"{defender.DiscordId}-{defender.Name}" attacker hackItems isTrainer let stealMsg = if not isTrainer then $"{defender.Name} has **{defender.Bank} $GBT** we can take from them. " else "" let embed = @@ -81,9 +77,9 @@ let pickHack actionId attacker defender isTrainer = .WithDescription($"{stealMsg}Pick the hack you want to use.") if not isTrainer then - for h in Player.getHacks attacker |> Array.sortBy (fun i -> i.Power) do - let amount = if h.Power > int defender.Bank then int defender.Bank else h.Power - embed.AddField(h.Name, $"Cooldown {h.Cooldown} mins\nExtract {amount} $GBT", true) |> ignore + for (item,power,hClass,cooldown) in Player.getHacks attacker do + let amount = if power > int defender.Bank then int defender.Bank else power + embed.AddField(item.Name, $"Cooldown {cooldown} mins\nExtract {amount} $GBT", true) |> ignore DiscordFollowupMessageBuilder() .AddComponents(buttons) @@ -93,7 +89,7 @@ let pickHack actionId attacker defender isTrainer = let responseSuccessfulHack earnedMoney (targetId : uint64) amountTaken (hack : Item) = let embed = DiscordEmbedBuilder() - .WithImageUrl(getHackGif (enum(hack.Id))) + .WithImageUrl(getItemGif hack.Id) .WithTitle("Hack Attack") .WithDescription($"You successfully hacked <@{targetId}> using {hack.Name}" + (if earnedMoney then $", and took {amountTaken} 💰$GBT from them!" else "!")) @@ -102,10 +98,10 @@ let responseSuccessfulHack earnedMoney (targetId : uint64) amountTaken (hack : I .AddEmbed(embed.Build()) .AsEphemeral(true) -let responseCreatedShield (shield : Item) = - let embed = DiscordEmbedBuilder().WithImageUrl(getShieldGif (enum(shield.Id))) +let responseCreatedShield ((item,_,cooldown) : ShieldItem) = + let embed = DiscordEmbedBuilder().WithImageUrl(getItemGif item.Id) embed.Title <- "Mounted Shield" - embed.Description <- $"Mounted {shield.Name} shield for {TimeSpan.FromMinutes(int shield.Cooldown).Hours} hours" + embed.Description <- $"Mounted {item.Name} shield for {TimeSpan.FromMinutes(int cooldown).Hours} hours" DiscordFollowupMessageBuilder() .AddEmbed(embed) @@ -115,60 +111,52 @@ let eventSuccessfulHack (ctx : IDiscordContext) target prize = DiscordMessageBuilder() .WithContent($"{ctx.GetDiscordMember().Username} successfully hacked <@{target.DiscordId}> and took {prize} GoodBoyTokenz") -let getBuyItemsEmbed (player : PlayerData) (itemType : ItemType) (store : Item array) = +let getBuyItemsEmbed (playerInventory : Inventory) (storeInventory : Inventory) = let embeds , buttons = - store - |> Array.filter (fun i -> i.Type = itemType) - |> Array.map (fun item -> + storeInventory + |> List.map (fun item -> let embed = DiscordEmbedBuilder() match item.Type with - | ItemType.Hack -> - embed - .AddField($"$GBT Reward |", string item.Power, true) - .AddField("Cooldown |", $"{TimeSpan.FromMinutes(int item.Cooldown).Minutes} minutes", true) - .WithThumbnail(getHackIcon (enum(item.Id))) - |> ignore - | _ -> - embed + | Hack(power,_,cooldown) -> + embed.AddField($"$GBT Reward |", string power, true) + .AddField("Cooldown |", $"{TimeSpan.FromMinutes(int cooldown).Minutes} minutes", true) + .WithThumbnail(getItemIcon item.Id) + |> ignore + | Shield(shieldClass,cooldown) -> + embed.AddField($"Strong against |", WeaponClass.getGoodAgainst shieldClass |> snd |> string, true) // .AddField($"Defensive Strength |", string item.Power, true) - .AddField($"Strong against |", WeaponClass.getGoodAgainst item.Class |> snd |> string, true) - .AddField("Active For |", $"{TimeSpan.FromMinutes(int item.Cooldown).Hours} hours", true) - .WithThumbnail(getShieldIcon (enum(item.Id))) - |> ignore + .AddField("Active For |", $"{TimeSpan.FromMinutes(int cooldown).Hours} hours", true) + .WithThumbnail(getItemIcon item.Id) + |> ignore + | _ -> () embed .AddField("Price 💰", (if item.Price = 0 then "Free" else $"{item.Price} $GBT"), true) - .WithColor(WeaponClass.getClassEmbedColor item.Class) + .WithColor(WeaponClass.getClassEmbedColor item) .WithTitle($"{item.Name}") |> ignore let button = - if player.Inventory |> Array.exists (fun i -> i.Id = item.Id) - then DiscordButtonComponent(WeaponClass.getClassButtonColor item.Class, $"Buy-{item.Id}", $"Own {item.Name}", true) - else DiscordButtonComponent(WeaponClass.getClassButtonColor item.Class, $"Buy-{item.Id}", $"Buy {item.Name}") - embed.Build() , button :> DiscordComponent) - |> Array.unzip + if playerInventory |> List.exists (fun i -> i.Id = item.Id) + then DiscordButtonComponent(WeaponClass.getClassButtonColor item, $"Buy-{item.Id}", $"Own {item.Name}", true) + else DiscordButtonComponent(WeaponClass.getClassButtonColor item, $"Buy-{item.Id}", $"Buy {item.Name}") + ( embed.Build() , button :> DiscordComponent )) + |> List.unzip DiscordFollowupMessageBuilder() .AddEmbeds(embeds) .AddComponents(buttons) .AsEphemeral(true) -let getSellEmbed (itemType : ItemType) (player : PlayerData) = +let getSellEmbed (items : Item list) = let embeds , buttons = - player.Inventory - |> Array.filter (fun i -> i.Type = itemType) - |> Array.map (fun item -> - let embed = DiscordEmbedBuilder() - match item.Type with - | ItemType.Hack -> embed.WithThumbnail(getHackIcon (enum(item.Id))) |> ignore - | _ -> embed.WithThumbnail(getShieldIcon (enum(item.Id))) |> ignore - embed + items + |> List.map (fun item -> + DiscordEmbedBuilder() .AddField("Sell For 💰", $"{item.Price} $GBT", true) .WithTitle($"{item.Name}") - .WithColor(WeaponClass.getClassEmbedColor item.Class) - |> ignore - let button = DiscordButtonComponent(WeaponClass.getClassButtonColor item.Class, $"Sell-{item.Id}", $"Sell {item.Name}") - embed.Build() , button :> DiscordComponent) - |> Array.unzip + .WithColor(WeaponClass.getClassEmbedColor item) + .Build() + , DiscordButtonComponent(WeaponClass.getClassButtonColor item, $"Sell-{id}", $"Sell {item.Name}") :> DiscordComponent) + |> List.unzip DiscordFollowupMessageBuilder() .AddEmbeds(embeds) diff --git a/Bot/GameHelpers.fs b/Bot/GameHelpers.fs index 0bac34a..abe5637 100644 --- a/Bot/GameHelpers.fs +++ b/Bot/GameHelpers.fs @@ -9,35 +9,80 @@ module Armory = let file = System.IO.File.ReadAllText("Items.json") // let file = System.IO.File.ReadAllText("Bot/Items.json") JsonConvert.DeserializeObject(file) + |> Array.toList - let hacks = battleItems |> Array.filter (fun bi -> match bi.Type with ItemType.Hack -> true | _ -> false) - let shields = battleItems |> Array.filter (fun bi -> match bi.Type with ItemType.Shield -> true | _ -> false) + let hacks = battleItems |> List.filter (fun item -> match item.Type with Hack _ -> true | _ -> false) + let shields = battleItems |> List.filter (fun item -> match item.Type with Shield _ -> true | _ -> false) - let getItem itemId = battleItems |> Array.find (fun w -> w.Id = itemId) + let getItem itemId = battleItems |> List.find (fun item -> item.Id = itemId) + + let getHackItems inventory = + inventory + |> List.filter (fun item -> match item.Type with Hack _ -> true | _ -> false) + |> List.sortBy (fun item -> item.Id) + let getShieldItems inventory = + inventory + |> List.filter (fun item -> match item.Type with Shield _ -> true | _ -> false) + |> List.sortBy (fun item -> item.Id) + let getHacks : HackItem list = + battleItems + |> List.choose (fun item -> + match item.Type with + | Hack(power, hackClass, cooldown) -> Some (item , power, hackClass, cooldown) | _ -> None) + |> List.sortBy (fun (item,_,_,_) -> item.Id) + let getShields : ShieldItem list = + battleItems + |> List.choose (fun item -> + match item.Type with + | Shield(hackClass, cooldown) -> Some (item,hackClass,cooldown) | _ -> None) + |> List.sortBy (fun (item,_,_) -> item.Id) + let getHackById id = getHacks |> List.find (fun (item,_,_,_) -> item.Id = id) + let getShieldById id = getShields |> List.find (fun (item,_,_) -> item.Id = id) module WeaponClass = // TODO: Find a different place to put this let SameTargetAttackCooldown = System.TimeSpan.FromHours(1) - let getClassButtonColor = function - | 0 -> ButtonStyle.Danger - | 1 -> ButtonStyle.Primary - | _ -> ButtonStyle.Success + let getClassButtonColor item = + match item.Type with + | Hack (_,0,_) | Shield (0,_) -> ButtonStyle.Danger + | Hack (_,1,_) | Shield (1,_) -> ButtonStyle.Primary + | Hack (_,2,_) | Shield (2,_) -> ButtonStyle.Success + | _ -> ButtonStyle.Primary - let getClassEmbedColor = function - | 0 -> DiscordColor.Red - | 1 -> DiscordColor.Blurple - | _ -> DiscordColor.Green + let getClassEmbedColor item = + match item.Type with + | Hack (_,0,_) | Shield (0,_) -> DiscordColor.Red + | Hack (_,1,_) | Shield (1,_) -> DiscordColor.Blurple + | Hack (_,2,_) | Shield (2,_) -> DiscordColor.Green + | _ -> DiscordColor.Blurple let getGoodAgainst = function - | 0 -> ( ShieldId.Firewall , HackId.Virus ) - | 1 -> ( ShieldId.Encryption , HackId.RemoteAccess ) - | _ -> ( ShieldId.Cypher , HackId.Worm ) + | 0 -> ( ItemId.Firewall , ItemId.Virus ) + | 1 -> ( ItemId.Encryption , ItemId.RemoteAccess ) + | _ -> ( ItemId.Cypher , ItemId.Worm ) module Player = - let getItems itemType (player : PlayerData) = player.Inventory |> Array.filter (fun i -> i.Type = itemType) - let getHacks (player : PlayerData) = getItems ItemType.Hack player - let getShields (player : PlayerData) = getItems ItemType.Shield player + let getHackItems player = + player.Inventory + |> List.filter (fun item -> match item.Type with Hack _ -> true | _ -> false) + |> List.sortBy (fun item -> item.Id) + let getShieldItems player = + player.Inventory + |> List.filter (fun item -> match item.Type with Shield _ -> true | _ -> false) + |> List.sortBy (fun item -> item.Id) + let getHacks player : HackItem list = + player.Inventory + |> List.choose (fun item -> + match item.Type with + | Hack(power, hackClass, cooldown) -> Some (item , power, hackClass, cooldown) | _ -> None) + |> List.sortBy (fun (item,_,_,_) -> item.Id) + let getShields player : ShieldItem list = + player.Inventory + |> List.choose (fun item -> + match item.Type with + | Shield(hackClass, cooldown) -> Some (item , hackClass, cooldown) | _ -> None) + |> List.sortBy (fun (item,_,_) -> item.Id) let getHackEvents player = player.Events |> Array.filter (fun act -> match act.Type with PlayerEventType.Hacking h -> h.IsInstigator | _ -> false) @@ -55,11 +100,17 @@ module Player = let modifyBank (player : PlayerData) amount = { player with Bank = max (player.Bank + amount) 0 } + let getStat statId player = + player.Stats + |> List.tryFind (fun stat -> statId = stat.Id) + |> Option.map (fun stat -> stat.Amount) + |> Option.defaultValue 0 + module Arsenal = - let battleItemFormat (items : Item array) = + let battleItemFormat (items : Item list) = match items with - | [||] -> "None" - | _ -> items |> Array.toList |> List.map (fun i -> i.Name) |> String.concat ", " + | [] -> "None" + | _ -> items |> List.map (fun item -> item.Name) |> String.concat ", " let actionFormat (actions : PlayerEvent array) = match actions with @@ -82,8 +133,8 @@ module Arsenal = let statusFormat p = let hacks = Player.getHackEvents p - $"**Hacks:** {Player.getHacks p |> battleItemFormat}\n - **Shields:** {Player.getShields p |> battleItemFormat}\n + $"**Hacks:** {Player.getHackItems p |> battleItemFormat}\n + **Shields:** {Player.getShieldItems p |> battleItemFormat}\n **Hack Attacks:**\n{ hacks |> Array.take (min hacks.Length 10) |> actionFormat}\n **Active Shields:**\n{Player.getShieldEvents p |> actionFormat}" diff --git a/Bot/GameTypes.fs b/Bot/GameTypes.fs index 0151b3d..4f15f61 100644 --- a/Bot/GameTypes.fs +++ b/Bot/GameTypes.fs @@ -1,111 +1,111 @@ -namespace Degenz +[] +module Degenz.Types open System -[] -module Types = +[] +type mins - [] - type mins +[] +type GBT - [] - type GBT +type ItemId = + | Virus = 0 + | RemoteAccess = 1 + | Worm = 2 + | Firewall = 6 + | Encryption = 7 + | Cypher = 8 - type HackId = - | Virus = 0 - | RemoteAccess = 1 - | Worm = 2 +type StatId = + | Strength = 0 + | Focus = 1 + | Luck = 2 + | Charisma = 3 - type ShieldId = - | Firewall = 6 - | Encryption = 7 - | Cypher = 8 +type Stat = { + Id : StatId + BaseDecayRate : single + BaseMinMax : Range +} - type ItemType = - | Hack = 0 - | Shield = 1 - | Food = 1 +type ActiveStat = { + Id : StatId + Amount : int +} - type ItemAttributes = { - Sell : bool - Buy : bool - Consume : bool - Drop : bool - } - with static member empty = { Sell = false ; Buy = false ; Consume = false ; Drop = false } +module PlayerStats = + let Strength = { Id = StatId.Strength ; BaseDecayRate = 5.0f ; BaseMinMax = Range(0, 100) } + let Focus = { Id = StatId.Focus ; BaseDecayRate = 5.0f ; BaseMinMax = Range(0, 100) } + let Luck = { Id = StatId.Luck ; BaseDecayRate = 5.0f ; BaseMinMax = Range(0, 100) } + let Charisma = { Id = StatId.Charisma ; BaseDecayRate = 5.0f ; BaseMinMax = Range(0, 100) } - type Item = { - Id : int - Name : string - Price : int - Type : ItemType - Power : int - Class : int - Cooldown : int - Attributes : ItemAttributes - } - with static member empty = - { Id = -1 - Name = "None" - Price = 0 - Type = ItemType.Hack - Power = 0 - Class = -1 - Cooldown = 0 - Attributes = ItemAttributes.empty } + let stats = [| Strength ; Focus ; Luck ; Charisma |] + let statConsumableMap = + [ ( StatId.Strength , 12 ) + ( StatId.Focus , 13 ) + ( StatId.Luck , 14 ) + ( StatId.Charisma , 15 ) ] - type HackResult = - | Strong - | Weak +type HackResult = + | Strong + | Weak - [] - type DiscordPlayer = { Id: uint64; Name: string } - with static member empty = { Id = 0uL ; Name = "None" } +[] +type DiscordPlayer = { Id: uint64; Name: string } + with static member empty = { Id = 0uL ; Name = "None" } - type HackEvent = { - IsInstigator : bool - Adversary : DiscordPlayer - Success : bool - HackId : int - } +type HackEvent = { + IsInstigator : bool + Adversary : DiscordPlayer + Success : bool + HackId : int +} - type PlayerEventType = - | Hacking of HackEvent - | Shielding of shieldId : int - | Stealing of instigator : bool * adversary : DiscordPlayer - | Imprison +type PlayerEventType = + | Hacking of HackEvent + | Shielding of shieldId : int + | Stealing of instigator : bool * adversary : DiscordPlayer + | Imprison - type PlayerEvent = - { Type : PlayerEventType - Cooldown : int - Timestamp : DateTime } +type PlayerEvent = + { Type : PlayerEventType + Cooldown : int + Timestamp : DateTime } - type PlayerTraits = { - Strength : int - Focus : int - Luck : int - Charisma : int - } - with static member empty = { Strength = 0 ; Focus = 0 ; Luck = 0 ; Charisma = 0 } - - [] - type PlayerData = { - DiscordId : uint64 - Name : string - Inventory : Item array - Events : PlayerEvent array - Traits : PlayerTraits - Bank : int - } +type HackItem = Item * int * int * int +and ShieldItem = Item * int * int +and FoodItem = Item * (Item -> PlayerData -> PlayerData) +and AccessoryItem = Item * (Item -> PlayerData -> PlayerData) +and ItemType = + | Hack of power : int * hackClass : int * cooldown : int + | Shield of shieldClass : int * cooldown : int + | Food of effect : (Item -> PlayerData -> PlayerData) + | Accessory of effect : (Item -> PlayerData -> PlayerData) +and Item = { + Id : int + Name : string + Price : int + Type : ItemType +} +and Inventory = Item list +and PlayerData = { + DiscordId : uint64 + Name : string + Inventory : Inventory + Events : PlayerEvent array + Stats : ActiveStat list + Bank : int +} // Achievements : string array // XP : int - with member this.basicPlayer = { Id = this.DiscordId ; Name = this.Name } - static member empty = - { DiscordId = 0uL - Name = "None" - Inventory = [||] - Events = [||] - Traits = PlayerTraits.empty +with member this.basicPlayer = { Id = this.DiscordId ; Name = this.Name } + static member empty = + { DiscordId = 0uL + Name = "None" + Inventory = [] + Events = [||] + Stats = [] // Achievements = [||] // XP = 0 - Bank = 0 } + Bank = 0 } diff --git a/Bot/Games/HackerBattle.fs b/Bot/Games/HackerBattle.fs index b710e28..f6c3fae 100644 --- a/Bot/Games/HackerBattle.fs +++ b/Bot/Games/HackerBattle.fs @@ -45,21 +45,21 @@ let checkWeaponHasCooldown (weapon : Item) attacker = let checkHasEmptyHacks attacker = match Player.getHacks attacker with - | [||] -> Error $"You currently do not have any Hacks to take 💰$GBT from others. Please go to the <#{GuildEnvironment.channelArmory}> and purchase one." + | [] -> Error $"You currently do not have any Hacks to take 💰$GBT from others. Please go to the <#{GuildEnvironment.channelArmory}> and purchase one." | _ -> Ok attacker let checkPlayerOwnsWeapon (item : Item) player = - match player.Inventory |> Array.exists (fun i -> i.Id = item.Id) with + match player.Inventory |> List.exists (fun i -> i.Id = item.Id) with | true -> Ok player | false -> Error $"You sold your weapon already, you cheeky bastard..." -let checkPlayerHasShieldSlotsAvailable (shield : Item) player = +let checkPlayerHasShieldSlotsAvailable player = let updatedPlayer = player |> Player.removeExpiredActions let defenses = Player.getShieldEvents updatedPlayer match defenses |> Array.length >= 3 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 + let event = defenses |> Array.rev |> Array.head // This should be the next expiring timestamp + let cooldown = getTimeText true (TimeSpan.FromMinutes(int event.Cooldown)) event.Timestamp Error $"You are only allowed three shields at a time. Wait {cooldown} to add another shield" | false -> Ok updatedPlayer @@ -68,35 +68,36 @@ let checkTargetHasFunds target player = | true -> Error $"Looks like the poor bastard has no $GBT... pick a different victim." | false -> Ok player -let calculateDamage (hack : Item) (shield : Item) = - if hack.Class = shield.Class +let calculateDamage ((_,_,hackClass,_) : HackItem) ((_,shieldClass,_) : ShieldItem) = + if hackClass = shieldClass then Weak else Strong -let runHackerBattle defender hack = +let runHackerBattle defender (hack : HackItem) = defender |> Player.removeExpiredActions |> fun p -> p.Events |> Array.choose (fun event -> match event.Type with - | Shielding id -> Armory.battleItems |> Array.find (fun w -> w.Id = id) |> Some + | Shielding id -> Armory.getShields |> List.find (fun (item,_,_) -> item.Id = id) |> Some | _ -> None) |> Array.map (calculateDamage hack) |> Array.contains Weak -let updateCombatants successfulHack (attacker : PlayerData) (defender : PlayerData) (hack : Item) prize = +let updateCombatants successfulHack (attacker : PlayerData) (defender : PlayerData) (hack : HackItem) prize = + let (item, power, hackClass, cooldown) = hack let updatePlayer amount attack p = { p with Events = Array.append [| attack |] p.Events ; Bank = max (p.Bank + amount) 0 } let event isDefenderEvent = let hackEvent = { - HackId = hack.Id + HackId = item.Id Adversary = if isDefenderEvent then attacker.basicPlayer else defender.basicPlayer IsInstigator = not isDefenderEvent Success = successfulHack } { Type = Hacking hackEvent Timestamp = DateTime.UtcNow - Cooldown = if isDefenderEvent then int WeaponClass.SameTargetAttackCooldown.TotalMinutes * 1 else hack.Cooldown } + Cooldown = if isDefenderEvent then int WeaponClass.SameTargetAttackCooldown.TotalMinutes * 1 else cooldown } [ DbService.updatePlayer GuildEnvironment.pgDb <| updatePlayer prize (event false) attacker DbService.updatePlayer GuildEnvironment.pgDb <| updatePlayer -prize (event true) defender @@ -105,12 +106,13 @@ let updateCombatants successfulHack (attacker : PlayerData) (defender : PlayerDa |> Async.Parallel |> Async.Ignore -let successfulHack (ctx : IDiscordContext) attacker defender hack = +let successfulHack (ctx : IDiscordContext) attacker defender (hack : HackItem) = async { - let prizeAmount = if hack.Power < int defender.Bank then hack.Power else int defender.Bank + let (item,power,hackClass,cooldown) = hack + let prizeAmount = if power < int defender.Bank then power else int defender.Bank do! updateCombatants true attacker defender hack (prizeAmount * 1) - let embed = Embeds.responseSuccessfulHack true defender.DiscordId prizeAmount hack + let embed = Embeds.responseSuccessfulHack true defender.DiscordId prizeAmount item do! ctx.FollowUp embed |> Async.AwaitTask let builder = Embeds.eventSuccessfulHack ctx defender prizeAmount @@ -120,9 +122,10 @@ let successfulHack (ctx : IDiscordContext) attacker defender hack = |> Async.Ignore } -let failedHack (ctx : IDiscordContext) attacker defender hack = +let failedHack (ctx : IDiscordContext) attacker defender (hack : HackItem) = + let (item, power, hackClass, cooldown) = hack async { - let lostAmount = if hack.Power < int attacker.Bank then hack.Power else int attacker.Bank + let lostAmount = if power < int attacker.Bank then power else int attacker.Bank let msg = $"Hack failed! {defender.Name} was able to mount a successful defense! You lost {lostAmount} $GBT!" do! sendFollowUpMessage ctx msg @@ -155,7 +158,7 @@ let handleAttack (ctx : IDiscordContext) = executePlayerAction ctx (fun attacker -> async { let tokens = ctx.GetInteractionId().Split("-") let hackId = int tokens.[1] - let hack = Armory.getItem hackId + let item,_,_,_ as hackItem = Armory.getHackById hackId let resultId , targetId = UInt64.TryParse tokens.[2] let! resultTarget = DbService.tryFindPlayer GuildEnvironment.pgDb targetId @@ -164,21 +167,21 @@ let handleAttack (ctx : IDiscordContext) = do! attacker |> Player.removeExpiredActions |> checkAlreadyHackedTarget defender - >>= checkPlayerOwnsWeapon hack - >>= checkWeaponHasCooldown hack + >>= checkPlayerOwnsWeapon item + >>= checkWeaponHasCooldown item |> function | Ok atkr -> - runHackerBattle defender hack + runHackerBattle defender hackItem |> function - | false -> successfulHack ctx atkr defender hack - | true -> failedHack ctx attacker defender hack + | false -> successfulHack ctx atkr defender hackItem + | true -> failedHack ctx attacker defender hackItem | Error msg -> Messaging.sendFollowUpMessage ctx msg | _ -> do! Messaging.sendFollowUpMessage ctx "Error occurred processing attack" }) let defend (ctx : IDiscordContext) = executePlayerAction ctx (fun player -> async { - if Player.getShields player |> Array.length > 0 then + if Player.getShields player |> List.length > 0 then let p = Player.removeExpiredActions player let embed = Embeds.pickDefense "Defend" p false do! ctx.FollowUp embed |> Async.AwaitTask @@ -191,18 +194,18 @@ let handleDefense (ctx : IDiscordContext) = executePlayerAction ctx (fun player -> async { let tokens = ctx.GetInteractionId().Split("-") let shieldId = int tokens.[1] - let shield = Armory.getItem shieldId + let item, shieldClass, cooldown as shield = Armory.getShields |> List.find (fun (item,_,_) -> item.Id = shieldId) do! player - |> checkPlayerOwnsWeapon shield - >>= checkPlayerHasShieldSlotsAvailable shield - >>= checkWeaponHasCooldown shield + |> checkPlayerOwnsWeapon item + >>= checkPlayerHasShieldSlotsAvailable + >>= checkWeaponHasCooldown item |> handleResultWithResponse ctx (fun p -> async { let embed = Embeds.responseCreatedShield shield do! ctx.FollowUp embed |> Async.AwaitTask let defense = { Type = Shielding shieldId - Cooldown = shield.Cooldown + Cooldown = cooldown Timestamp = DateTime.UtcNow } do! DbService.updatePlayer GuildEnvironment.pgDb p diff --git a/Bot/Games/Store.fs b/Bot/Games/Store.fs index c105a40..26caf93 100644 --- a/Bot/Games/Store.fs +++ b/Bot/Games/Store.fs @@ -15,30 +15,31 @@ let checkHasSufficientFunds (item : Item) player = else Error $"You do not have sufficient funds to buy this item! Current balance: {player.Bank} GBT" let checkAlreadyOwnsItem (item : Item) player = - if player.Inventory |> Array.exists (fun w -> item.Id = w.Id) + if player.Inventory |> List.exists (fun w -> item.Id = w.Id) then Error $"You already own {item.Name}!" else Ok player -let checkSoldItemAlready (item : Item) player = - if player.Inventory |> Array.exists (fun w -> item.Id = w.Id) +let checkSoldItemAlready item player = + if player.Inventory |> List.exists (fun i -> item.Id = i.Id) then Ok player - else Error $"{item.Name} not found in your arsenal! Looks like you sold it already." + else Error $"{item.Name} not found in your inventory! Looks like you sold it already." -let checkHasItemsInArsenal itemType player = - if player.Inventory |> Array.filter (fun i -> i.Type = itemType ) |> Array.length > 0 +let checkHasItemsInArsenal itemType items player = + if List.isEmpty items |> not then Ok player - else Error $"You currently have no {itemType}s in your arsenal to sell!" + else Error $"You currently have no {itemType} in your arsenal to sell!" -let buy itemType (ctx : IDiscordContext) = +let buy getItems (ctx : IDiscordContext) = executePlayerAction ctx (fun player -> async { - let itemStore = Embeds.getBuyItemsEmbed player itemType Armory.battleItems + let itemStore = Embeds.getBuyItemsEmbed (getItems player.Inventory) (getItems Armory.battleItems) do! ctx.FollowUp itemStore |> Async.AwaitTask }) -let sell itemType (ctx : IDiscordContext) = +let sell itemType getItems (ctx : IDiscordContext) = executePlayerAction ctx (fun player -> async { - match checkHasItemsInArsenal itemType player with - | Ok _ -> let itemStore = Embeds.getSellEmbed itemType player + let items = getItems player + match checkHasItemsInArsenal itemType items player with + | Ok _ -> let itemStore = Embeds.getSellEmbed items do! ctx.FollowUp(itemStore) |> Async.AwaitTask | Error e -> do! sendFollowUpMessage ctx e }) @@ -46,13 +47,13 @@ let sell itemType (ctx : IDiscordContext) = // TODO: When you buy a shield, prompt the user to activate it let handleBuyItem (ctx : IDiscordContext) itemId = executePlayerAction ctx (fun player -> async { - let item = Armory.battleItems |> Array.find (fun w -> w.Id = itemId) + let item = Armory.getItem itemId do! player |> checkHasSufficientFunds item >>= checkAlreadyOwnsItem item |> handleResultWithResponse ctx (fun player -> async { let newBalance = player.Bank - item.Price - let p = { player with Bank = newBalance ; Inventory = Array.append [| item |] player.Inventory } + let p = { player with Bank = newBalance ; Inventory = item::player.Inventory } do! DbService.updatePlayer GuildEnvironment.pgDb p |> Async.Ignore do! sendFollowUpMessage ctx $"Successfully purchased {item.Name}! You now have {newBalance} 💰$GBT remaining" }) @@ -68,7 +69,7 @@ let handleSell (ctx : IDiscordContext) itemId = let updatedPlayer = { player with Bank = player.Bank + item.Price - Inventory = player.Inventory |> Array.filter (fun i -> i.Id <> itemId) + Inventory = player.Inventory |> List.filter (fun i -> i.Id <> itemId) } do! [ DbService.updatePlayer GuildEnvironment.pgDb updatedPlayer |> Async.Ignore @@ -76,8 +77,7 @@ let handleSell (ctx : IDiscordContext) itemId = sendFollowUpMessage ctx $"Sold {item.Type} {item.Name} for {item.Price}! Current Balance: {updatedPlayer.Bank}" ] |> Async.Parallel |> Async.Ignore - - }) + }) }) let handleStoreEvents (_ : DiscordClient) (event : ComponentInteractionCreateEventArgs) = @@ -108,14 +108,14 @@ type Store() = } [] - member _.BuyHack (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (buy ItemType.Hack) + member _.BuyHack (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (buy Armory.getHackItems) [] - member this.BuyShield (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (buy ItemType.Shield) + member this.BuyShield (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (buy Armory.getShieldItems) [] - member this.SellHack (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (sell ItemType.Hack) + member this.SellHack (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (sell "Hacks" Player.getHackItems) [] - member this.SellShield (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (sell ItemType.Shield) + member this.SellShield (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (sell "Shields" Player.getShieldItems) diff --git a/Bot/Games/Thief.fs b/Bot/Games/Thief.fs index fd1a196..b94ed29 100644 --- a/Bot/Games/Thief.fs +++ b/Bot/Games/Thief.fs @@ -128,7 +128,9 @@ let steal target amount (ctx : IDiscordContext) = >>= checkPrizeRequestZero amount |> handleResultWithResponse ctx (fun _ -> async { let cappedPrize , winPercentage , wasCapped = - calculateWinPercentage amount (int victim.Bank) thief.Traits.Strength victim.Traits.Strength +// calculateWinPercentage amount (int victim.Bank) thief.Stats.Strength victim.Stats.Strength + // TODO: Readd stats + calculateWinPercentage amount (int victim.Bank) 0 0 let chance = int (winPercentage * 100.0) let buttons = @@ -138,7 +140,8 @@ let steal target amount (ctx : IDiscordContext) = let cappedMsg = if wasCapped then $"They only have {cappedPrize} $GBT though... " else "" let strengthMsg = - match thief.Traits.Strength - victim.Traits.Strength with + // TODO: Readd stats + match 0 - 0 with | diff when diff < -50 -> "much stronger" | diff when diff < 0 -> "stronger" | diff when diff < 50 -> "weaker" @@ -161,7 +164,8 @@ let handleSteal (ctx : IDiscordContext) = let targetId = uint64 tokens.[2] let targetName = tokens.[3] let amount = int tokens.[4] - let prize , winPercentage , _ = calculateWinPercentage amount (int victim.Bank) thief.Traits.Strength victim.Traits.Strength + // TODO: Readd stats + let prize , winPercentage , _ = calculateWinPercentage amount (int victim.Bank) 0 0 let prize = int prize * 1 let rand = Random(Guid.NewGuid().GetHashCode()) diff --git a/Bot/Games/Trainer.fs b/Bot/Games/Trainer.fs index e8958e9..a0aa6c1 100644 --- a/Bot/Games/Trainer.fs +++ b/Bot/Games/Trainer.fs @@ -9,8 +9,8 @@ open Degenz.Messaging let trainerAchievement = "FINISHED_TRAINER" let Sensei = { Id = GuildEnvironment.botIdHackerBattle ; Name = "Sensei" } -let defaultHack = Armory.battleItems |> Array.find (fun i -> i.Id = int HackId.Virus) -let defaultShield = Armory.battleItems |> Array.find (fun i -> i.Id = int ShieldId.Firewall) +let defaultHackItem, hackPower, hackClass, hackCooldown as defaultHack = Armory.getHackById (int ItemId.Virus) +let defaultShieldItem, shieldClass, shieldCooldown as defaultShield = Armory.getShieldById (int ItemId.Firewall) let TrainerEvents = [| { Timestamp = System.DateTime.UtcNow @@ -19,10 +19,10 @@ let TrainerEvents = [| Adversary = Sensei Success = true IsInstigator = true - HackId = defaultHack.Id } } + HackId = defaultHackItem.Id } } { Timestamp = System.DateTime.UtcNow - Cooldown = defaultShield.Cooldown - Type = Shielding defaultShield.Id } + Cooldown = shieldCooldown + Type = Shielding defaultShieldItem.Id } |] let sendInitialEmbed (client : DiscordClient) = @@ -52,7 +52,7 @@ let handleTrainerStep1 (ctx : IDiscordContext) = |> Async.AwaitTask let msg = "Beautopia© is a dangerous place... quick, put up a SHIELD 🛡 before another Degen hacks you, and takes your 💰$GBT.\n\n" + "To enable it, you need to run the `/shield` slash command.\n\n" - + $"Type the `/shield` command now, then select - `{defaultShield.Name}`\n" + + $"Type the `/shield` command now, then select - `{defaultShieldItem.Name}`\n" let builder = DiscordInteractionResponseBuilder() .WithContent(msg) @@ -66,7 +66,7 @@ let defend (ctx : IDiscordContext) = do! Messaging.defer ctx let m = ctx.GetDiscordMember() let name = if System.String.IsNullOrEmpty m.Nickname then m.DisplayName else m.Nickname - let embed = Embeds.pickDefense "Trainer-2" { PlayerData.empty with Inventory = [| defaultShield |] ; Name = name } true + let embed = Embeds.pickDefense "Trainer-2" { PlayerData.empty with Inventory = [ defaultShieldItem ] ; Name = name } true do! ctx.FollowUp(embed) |> Async.AwaitTask } |> Async.StartAsTask :> Task @@ -83,17 +83,16 @@ let handleDefense (ctx : IDiscordContext) = let sendMessage' = sendFollowUpMessage ctx let tokens = ctx.GetInteractionId().Split("-") - let shieldId = enum(int tokens.[2]) - let shield = Armory.getItem (int shieldId) + let shieldId = enum(int tokens.[2]) let playerName = tokens.[4] - let embed = Embeds.responseCreatedShield shield + let embed = Embeds.responseCreatedShield defaultShield do! ctx.FollowUp embed |> Async.AwaitTask do! Async.Sleep 4000 - do! sendMessage' $"Ok, good, let me make sure that worked.\n\nI'll try to **hack** you now with **{defaultHack.Name}**" + do! sendMessage' $"Ok, good, let me make sure that worked.\n\nI'll try to **hack** you now with **{defaultHackItem.Name}**" do! Async.Sleep 5000 do! sendMessage' $"❌ HACKING FAILED!\n\n{playerName} defended hack from <@{Sensei.Id}>!" do! Async.Sleep 4000 - do! sendFollowUpMessageWithButton ctx (handleDefenseMsg defaultHack.Name) + do! sendFollowUpMessageWithButton ctx (handleDefenseMsg defaultHackItem.Name) } |> Async.StartAsTask :> Task let handleTrainerStep3 (ctx : IDiscordContext) = @@ -104,7 +103,7 @@ let handleTrainerStep3 (ctx : IDiscordContext) = .WithContent ( "Now let’s **HACK** 💻... I want you to **HACK ME**!\n\n" + "To **hack**, you need to run the `/hack` slash command.\n" - + $"Type the `/hack` command now, then choose me - <@{Sensei.Id}> as your target, and select `{defaultHack.Name}`") + + $"Type the `/hack` command now, then choose me - <@{Sensei.Id}> as your target, and select `{defaultHackItem.Name}`") do! ctx.Respond(InteractionResponseType.ChannelMessageWithSource, builder) |> Async.AwaitTask } |> Async.StartAsTask :> Task @@ -117,7 +116,7 @@ let hack (target : DiscordUser) (ctx : IDiscordContext) = match isRightTarget with | true -> let bot = { PlayerData.empty with DiscordId = Sensei.Id ; Name = Sensei.Name } - let embed = Embeds.pickHack "Trainer-4" { PlayerData.empty with Inventory = [| defaultHack |] } bot true + let embed = Embeds.pickHack "Trainer-4" { PlayerData.empty with Inventory = [ defaultHackItem ] } bot true do! ctx.FollowUp(embed) |> Async.AwaitTask | false -> @@ -133,7 +132,7 @@ let handleHack (ctx : IDiscordContext) = PlayerInteractions.executePlayerAction ctx (fun player -> async { let sendMessage' = sendFollowUpMessage ctx do! Async.Sleep 1000 - let embed = Embeds.responseSuccessfulHack false Sensei.Id defaultHack.Power defaultHack + let embed = Embeds.responseSuccessfulHack false Sensei.Id hackPower defaultHackItem do! ctx.FollowUp(embed) |> Async.AwaitTask do! Async.Sleep 4000 do! sendMessage' @@ -151,7 +150,7 @@ let handleHack (ctx : IDiscordContext) = do! DbService.addAchievement GuildEnvironment.pgDb player.DiscordId trainerAchievement |> Async.Ignore - sb.Append($"I'm going to gift you a hack,`{defaultHack.Name}` and a shield, `{defaultShield.Name}`") |> ignore + sb.Append($"I'm going to gift you a hack,`{defaultHackItem.Name}` and a shield, `{defaultShieldItem.Name}`") |> 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 @@ -166,12 +165,12 @@ let handleHack (ctx : IDiscordContext) = let handleArsenal (ctx : IDiscordContext) = PlayerInteractions.executePlayerAction ctx (fun player -> async { - let hasStockWeapons = Player.getHacks player |> Array.exists (fun item -> item.Id = defaultHack.Id) + let hasStockWeapons = Player.getHackItems player |> List.exists (fun item -> item.Id = defaultHackItem.Id) let updatedPlayer = if not hasStockWeapons then { Player.removeExpiredActions player with Events = TrainerEvents |> Array.append player.Events - Inventory = [| defaultHack ; defaultShield |] |> Array.append player.Inventory + Inventory = defaultHackItem::defaultShieldItem::player.Inventory } else Player.removeExpiredActions player @@ -188,7 +187,7 @@ let handleArsenal (ctx : IDiscordContext) = let! completed = DbService.checkHasAchievement GuildEnvironment.pgDb player.DiscordId trainerAchievement if not completed then do! Async.Sleep 3000 - let rewards = [ $"{defaultHack.Name} Hack" ; $"{defaultShield.Name} Shield" ] + let rewards = [ $"{defaultHackItem.Name} Hack" ; $"{defaultShieldItem.Name} Shield" ] let embed = Embeds.getAchievementEmbed rewards "You completed the Training Dojo and collected loot." trainerAchievement do! ctx.FollowUp(embed) |> Async.AwaitTask do! Async.Sleep 2000 diff --git a/Bot/Items.json b/Bot/Items.json index 238bb0b..5ba6228 100644 --- a/Bot/Items.json +++ b/Bot/Items.json @@ -2,91 +2,76 @@ { "Id": 0, "Name": "Virus", - "Type": 0, "Price": 0, - "Power": 25, - "Cooldown": 1, - "Class": 0, - "Attributes": { - "Sell": false, - "Buy": false, - "Consume": false, - "Drop": false + "Type": { + "Case": "Hack", + "Fields": [ + 25, + 0, + 1 + ] } }, { "Id": 1, "Name": "RemoteAccess", - "Type": 0, "Price": 500, - "Power": 75, - "Cooldown": 3, - "Class": 1, - "Attributes": { - "Sell": true, - "Buy": true, - "Consume": false, - "Drop": true + "Type": { + "Case": "Hack", + "Fields": [ + 75, + 1, + 3 + ] } }, { "Id": 2, "Name": "Worm", - "Type": 0, "Price": 5000, - "Power": 150, - "Cooldown": 5, - "Class": 2, - "Attributes": { - "Sell": true, - "Buy": true, - "Consume": false, - "Drop": true + "Type": { + "Case": "Hack", + "Fields": [ + 150, + 2, + 5 + ] } }, { "Id": 6, "Name": "Firewall", - "Type": 1, "Price": 0, - "Power": 10, - "Class": 0, - "Cooldown": 120, - "Attributes": { - "Sell": false, - "Buy": false, - "Consume": false, - "Drop": false + "Type": { + "Case": "Shield", + "Fields": [ + 0, + 120 + ] } }, { "Id": 7, "Name": "Encryption", - "Type": 1, "Price": 500, - "Power": 50, - "Class": 1, - "Cooldown": 240, - "Attributes": { - "Sell": true, - "Buy": true, - "Consume": false, - "Drop": true + "Type": { + "Case": "Shield", + "Fields": [ + 1, + 240 + ] } }, { "Id": 8, "Name": "Cypher", - "Type": 1, "Price": 5000, - "Power": 80, - "Class": 2, - "Cooldown": 380, - "Attributes": { - "Sell": true, - "Buy": true, - "Consume": false, - "Drop": true + "Type": { + "Case": "Shield", + "Fields": [ + 2, + 380 + ] } } ] diff --git a/Bot/PlayerInteractions.fs b/Bot/PlayerInteractions.fs index bfe188e..eaa0623 100644 --- a/Bot/PlayerInteractions.fs +++ b/Bot/PlayerInteractions.fs @@ -3,7 +3,6 @@ module Degenz.PlayerInteractions open System.Threading.Tasks open DSharpPlus open DSharpPlus.Entities -open DSharpPlus.SlashCommands open Degenz.Messaging open Degenz.DbService @@ -57,52 +56,3 @@ let handleResultWithResponse ctx fn (player : Result) = match player with | Ok p -> fn p | Error e -> async { do! Messaging.sendFollowUpMessage ctx e } - -module Commands = - let newPlayer nickname (membr : uint64) = - let rand = System.Random(System.Guid.NewGuid().GetHashCode()) - let randHack = rand.Next(0, 3) - let randShield = rand.Next(6, 9) - let hack = Armory.battleItems |> Array.find (fun i -> i.Id = randHack) - let shield = Armory.battleItems |> Array.find (fun i -> i.Id = randShield) - - { DiscordId = membr - Name = nickname - Inventory = [| hack ; shield |] - Events = [||] -// XP = 0 -// Achievements = [||] - Traits = PlayerTraits.empty - Bank = 100 } - - let upsertPlayer discordId = - async { - let! player = DbService.tryFindPlayer GuildEnvironment.pgDb discordId - let! newPlayer = - match player with - | Some _ -> async.Return false - | None -> - async { -// do! newPlayer "" discordId |> DbService.insertNewPlayer - return true - } - return newPlayer - } - - [] - type LeaderboardEntry = { - Position : string - Amount : string - Name : string - } - -type PlayerInteractions() = - inherit ApplicationCommandModule () - - [] - member _.AddHackerRole (ctx : InteractionContext) = Commands.upsertPlayer ctx.Member.Id - -// [] -// member this.Leaderboard (ctx : InteractionContext) = Commands.leaderboard ctx - - diff --git a/Bot/XP.fs b/Bot/XP.fs index 0477d3a..b26c175 100644 --- a/Bot/XP.fs +++ b/Bot/XP.fs @@ -6,7 +6,7 @@ open DSharpPlus.Entities type RewardType = | Currency of int - | RandomItem of itemType : ItemType * amount : int +// | RandomItem of itemType : ItemType * amount : int | SpecialItem of id : int [] diff --git a/DbService/DbService.fsproj b/DbService/DbService.fsproj deleted file mode 100644 index e7786d7..0000000 --- a/DbService/DbService.fsproj +++ /dev/null @@ -1,11 +0,0 @@ - - - - net6.0 - true - - - - - - \ No newline at end of file diff --git a/DbService/paket.references b/DbService/paket.references deleted file mode 100644 index 8199177..0000000 --- a/DbService/paket.references +++ /dev/null @@ -1,8 +0,0 @@ -FSharp.Core -DSharpPlus -// DSharpPlus.CommandsNext -// DSharpPlus.Interactivity -DSharpPlus.SlashCommands - -MongoDB.Driver -Npgsql.FSharp diff --git a/Shared/Shared.fs b/Shared/Shared.fs deleted file mode 100644 index 8148165..0000000 --- a/Shared/Shared.fs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Degenz - -open System -open System.Threading.Tasks -open DSharpPlus -open DSharpPlus.Entities -open DSharpPlus.EventArgs -open DSharpPlus.SlashCommands -open Newtonsoft.Json diff --git a/Shared/Shared.fsproj b/Shared/Shared.fsproj deleted file mode 100644 index e855264..0000000 --- a/Shared/Shared.fsproj +++ /dev/null @@ -1,12 +0,0 @@ - - - - net6.0 - true - - - - - - - \ No newline at end of file diff --git a/Shared/paket.references b/Shared/paket.references deleted file mode 100644 index 71731ab..0000000 --- a/Shared/paket.references +++ /dev/null @@ -1,3 +0,0 @@ -FSharp.Core -DSharpPlus -DSharpPlus.SlashCommands From 9867d065121cf6f7c2734174a709fd22322fd305 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sun, 27 Feb 2022 22:33:15 +0700 Subject: [PATCH 05/28] Introduce denormalized records + mapping functions --- Bot/Embeds.fs | 78 +++++---------------------- Bot/GameHelpers.fs | 111 +++++++++++++++++++++++++------------- Bot/GameTypes.fs | 23 ++++++-- Bot/Games/HackerBattle.fs | 48 ++++++++--------- Bot/Games/Store.fs | 63 ++++++++++++++++++++-- Bot/Games/Trainer.fs | 38 +++++++------ 6 files changed, 206 insertions(+), 155 deletions(-) diff --git a/Bot/Embeds.fs b/Bot/Embeds.fs index 9fd59a4..c2a88f2 100644 --- a/Bot/Embeds.fs +++ b/Bot/Embeds.fs @@ -48,7 +48,7 @@ let constructButtons (actionId: string) (buttonInfo : string) (player: PlayerDat |> Seq.cast let pickDefense actionId player isTrainer = - let shieldItems = player |> Player.getShieldItems + let shieldItems = player.Inventory |> Inventory.filterByShields let buttons = constructButtons actionId (string player.DiscordId) player shieldItems isTrainer let embed = @@ -56,10 +56,10 @@ let pickDefense actionId player isTrainer = .WithTitle("Shield Defense") .WithDescription("Pick a shield to protect yourself from hacks") - for (item,sClass,cooldown) in Player.getShields player do - let hours = TimeSpan.FromMinutes(int cooldown).TotalHours - let against = WeaponClass.getGoodAgainst(sClass) |> snd - embed.AddField(item.Name, $"Active {hours} hours\nDefeats {against}", true) |> ignore + for shield in Inventory.getShieldItems player.Inventory do + let hours = TimeSpan.FromMinutes(int shield.Cooldown).TotalHours + let against = WeaponClass.getGoodAgainst(shield.Class) |> snd + embed.AddField(shield.Name, $"Active {hours} hours\nDefeats {against}", true) |> ignore DiscordFollowupMessageBuilder() .AddComponents(buttons) @@ -67,7 +67,7 @@ let pickDefense actionId player isTrainer = .AsEphemeral(true) let pickHack actionId attacker defender isTrainer = - let hackItems = attacker |> Player.getHackItems + let hackItems = attacker.Inventory |> Inventory.filterByHacks let buttons = constructButtons actionId $"{defender.DiscordId}-{defender.Name}" attacker hackItems isTrainer let stealMsg = if not isTrainer then $"{defender.Name} has **{defender.Bank} $GBT** we can take from them. " else "" @@ -77,16 +77,16 @@ let pickHack actionId attacker defender isTrainer = .WithDescription($"{stealMsg}Pick the hack you want to use.") if not isTrainer then - for (item,power,hClass,cooldown) in Player.getHacks attacker do - let amount = if power > int defender.Bank then int defender.Bank else power - embed.AddField(item.Name, $"Cooldown {cooldown} mins\nExtract {amount} $GBT", true) |> ignore + for hack in Inventory.getHackItems attacker.Inventory do + let amount = if hack.Power > int defender.Bank then int defender.Bank else hack.Power + embed.AddField(hack.Name, $"Cooldown {hack.Cooldown} mins\nExtract {amount} $GBT", true) |> ignore DiscordFollowupMessageBuilder() .AddComponents(buttons) .AddEmbeds([ DiscordEmbedBuilder().WithImageUrl(hackGif).Build() ; embed.Build() ]) .AsEphemeral true -let responseSuccessfulHack earnedMoney (targetId : uint64) amountTaken (hack : Item) = +let responseSuccessfulHack earnedMoney (targetId : uint64) amountTaken (hack : HackItem) = let embed = DiscordEmbedBuilder() .WithImageUrl(getItemGif hack.Id) @@ -98,10 +98,10 @@ let responseSuccessfulHack earnedMoney (targetId : uint64) amountTaken (hack : I .AddEmbed(embed.Build()) .AsEphemeral(true) -let responseCreatedShield ((item,_,cooldown) : ShieldItem) = - let embed = DiscordEmbedBuilder().WithImageUrl(getItemGif item.Id) +let responseCreatedShield (shield : ShieldItem) = + let embed = DiscordEmbedBuilder().WithImageUrl(getItemGif shield.Id) embed.Title <- "Mounted Shield" - embed.Description <- $"Mounted {item.Name} shield for {TimeSpan.FromMinutes(int cooldown).Hours} hours" + embed.Description <- $"Mounted {shield.Name} shield for {TimeSpan.FromMinutes(int shield.Cooldown).Hours} hours" DiscordFollowupMessageBuilder() .AddEmbed(embed) @@ -111,58 +111,6 @@ let eventSuccessfulHack (ctx : IDiscordContext) target prize = DiscordMessageBuilder() .WithContent($"{ctx.GetDiscordMember().Username} successfully hacked <@{target.DiscordId}> and took {prize} GoodBoyTokenz") -let getBuyItemsEmbed (playerInventory : Inventory) (storeInventory : Inventory) = - let embeds , buttons = - storeInventory - |> List.map (fun item -> - let embed = DiscordEmbedBuilder() - match item.Type with - | Hack(power,_,cooldown) -> - embed.AddField($"$GBT Reward |", string power, true) - .AddField("Cooldown |", $"{TimeSpan.FromMinutes(int cooldown).Minutes} minutes", true) - .WithThumbnail(getItemIcon item.Id) - |> ignore - | Shield(shieldClass,cooldown) -> - embed.AddField($"Strong against |", WeaponClass.getGoodAgainst shieldClass |> snd |> string, true) -// .AddField($"Defensive Strength |", string item.Power, true) - .AddField("Active For |", $"{TimeSpan.FromMinutes(int cooldown).Hours} hours", true) - .WithThumbnail(getItemIcon item.Id) - |> ignore - | _ -> () - embed - .AddField("Price 💰", (if item.Price = 0 then "Free" else $"{item.Price} $GBT"), true) - .WithColor(WeaponClass.getClassEmbedColor item) - .WithTitle($"{item.Name}") - |> ignore - let button = - if playerInventory |> List.exists (fun i -> i.Id = item.Id) - then DiscordButtonComponent(WeaponClass.getClassButtonColor item, $"Buy-{item.Id}", $"Own {item.Name}", true) - else DiscordButtonComponent(WeaponClass.getClassButtonColor item, $"Buy-{item.Id}", $"Buy {item.Name}") - ( embed.Build() , button :> DiscordComponent )) - |> List.unzip - - DiscordFollowupMessageBuilder() - .AddEmbeds(embeds) - .AddComponents(buttons) - .AsEphemeral(true) - -let getSellEmbed (items : Item list) = - let embeds , buttons = - items - |> List.map (fun item -> - DiscordEmbedBuilder() - .AddField("Sell For 💰", $"{item.Price} $GBT", true) - .WithTitle($"{item.Name}") - .WithColor(WeaponClass.getClassEmbedColor item) - .Build() - , DiscordButtonComponent(WeaponClass.getClassButtonColor item, $"Sell-{id}", $"Sell {item.Name}") :> DiscordComponent) - |> List.unzip - - DiscordFollowupMessageBuilder() - .AddEmbeds(embeds) - .AddComponents(buttons) - .AsEphemeral(true) - let getArsenalEmbed (player : PlayerData) = DiscordFollowupMessageBuilder() .AsEphemeral(true) diff --git a/Bot/GameHelpers.fs b/Bot/GameHelpers.fs index abe5637..2218b58 100644 --- a/Bot/GameHelpers.fs +++ b/Bot/GameHelpers.fs @@ -24,20 +24,60 @@ module Armory = inventory |> List.filter (fun item -> match item.Type with Shield _ -> true | _ -> false) |> List.sortBy (fun item -> item.Id) - let getHacks : HackItem list = - battleItems - |> List.choose (fun item -> - match item.Type with - | Hack(power, hackClass, cooldown) -> Some (item , power, hackClass, cooldown) | _ -> None) - |> List.sortBy (fun (item,_,_,_) -> item.Id) - let getShields : ShieldItem list = - battleItems - |> List.choose (fun item -> - match item.Type with - | Shield(hackClass, cooldown) -> Some (item,hackClass,cooldown) | _ -> None) - |> List.sortBy (fun (item,_,_) -> item.Id) - let getHackById id = getHacks |> List.find (fun (item,_,_,_) -> item.Id = id) - let getShieldById id = getShields |> List.find (fun (item,_,_) -> item.Id = id) + let getHackById id inventory = inventory |> getHackItems |> List.find (fun item -> item.Id = id) + let getShieldById id inventory = inventory |> getShieldItems |> List.find (fun item -> item.Id = id) + +module Inventory = + let itemToHack item power hackClass cooldown = { + Id = item.Id + Name = item.Name + Price = item.Price + Power = power + Class = hackClass + Cooldown = cooldown + } + + let itemToShield item hackClass cooldown = { + Id = item.Id + Name = item.Name + Price = item.Price + Class = hackClass + Cooldown = cooldown + } + let hackToItem (hack : HackItem) = { + Id = hack.Id + Name = hack.Name + Price = hack.Price + Type = Hack (hack.Power, hack.Class, hack.Cooldown) + } + let shieldToItem (shield : ShieldItem) = { + Id = shield.Id + Name = shield.Name + Price = shield.Price + Type = Shield (shield.Class, shield.Cooldown) + } + + let filterByHacks inventory = + inventory |> List.filter (fun item -> match item.Type with Hack _ -> true | _ -> false) + let filterByShields inventory = + inventory |> List.filter (fun item -> match item.Type with Shield _ -> true | _ -> false) + + let getHackItems inventory = + inventory + |> List.choose (fun item -> match item.Type with Hack (p,cl,co) -> Some (itemToHack item p cl co) | _ -> None) + |> List.sortBy (fun item -> item.Id) + let getShieldItems inventory = + inventory + |> List.choose (fun item -> match item.Type with Shield (cl,co) -> Some (itemToShield item cl co) | _ -> None) + |> List.sortBy (fun item -> item.Id) + let findHackById id inventory = + inventory |> getHackItems |> List.pick (fun item -> if item.Id = id then Some item else None) + let findShieldById id inventory = + inventory |> getShieldItems |> List.pick (fun item -> if item.Id = id then Some item else None) + let tryFindHackById id inventory = + inventory |> getHackItems |> List.tryFind (fun item -> item.Id = id) + let tryFindShieldById id inventory = + inventory |> getShieldItems |> List.tryFind (fun item -> item.Id = id) module WeaponClass = // TODO: Find a different place to put this @@ -63,26 +103,6 @@ module WeaponClass = | _ -> ( ItemId.Cypher , ItemId.Worm ) module Player = - let getHackItems player = - player.Inventory - |> List.filter (fun item -> match item.Type with Hack _ -> true | _ -> false) - |> List.sortBy (fun item -> item.Id) - let getShieldItems player = - player.Inventory - |> List.filter (fun item -> match item.Type with Shield _ -> true | _ -> false) - |> List.sortBy (fun item -> item.Id) - let getHacks player : HackItem list = - player.Inventory - |> List.choose (fun item -> - match item.Type with - | Hack(power, hackClass, cooldown) -> Some (item , power, hackClass, cooldown) | _ -> None) - |> List.sortBy (fun (item,_,_,_) -> item.Id) - let getShields player : ShieldItem list = - player.Inventory - |> List.choose (fun item -> - match item.Type with - | Shield(hackClass, cooldown) -> Some (item , hackClass, cooldown) | _ -> None) - |> List.sortBy (fun (item,_,_) -> item.Id) let getHackEvents player = player.Events |> Array.filter (fun act -> match act.Type with PlayerEventType.Hacking h -> h.IsInstigator | _ -> false) @@ -133,8 +153,27 @@ module Arsenal = let statusFormat p = let hacks = Player.getHackEvents p - $"**Hacks:** {Player.getHackItems p |> battleItemFormat}\n - **Shields:** {Player.getShieldItems p |> battleItemFormat}\n + $"**Hacks:** {Inventory.filterByHacks p.Inventory |> battleItemFormat}\n + **Shields:** {Inventory.filterByShields p.Inventory |> battleItemFormat}\n **Hack Attacks:**\n{ hacks |> Array.take (min hacks.Length 10) |> actionFormat}\n **Active Shields:**\n{Player.getShieldEvents p |> actionFormat}" +module Items = + let mapHack fn inventory = + inventory + |> List.choose (fun item -> + match item.Type with + | Hack(power, hackClass, cooldown) -> Some <| fn item power hackClass cooldown + | _ -> None) + let mapShield fn inventory = + inventory + |> List.choose (fun item -> + match item.Type with + | Shield(hackClass, cooldown) -> Some <| fn item hackClass cooldown + | _ -> None) + let doShields fn inventory = + inventory + |> List.iter (fun item -> + match item.Type with + | Shield(hackClass, cooldown) -> fn item hackClass cooldown + | _ -> ()) diff --git a/Bot/GameTypes.fs b/Bot/GameTypes.fs index 4f15f61..b9e9454 100644 --- a/Bot/GameTypes.fs +++ b/Bot/GameTypes.fs @@ -73,11 +73,24 @@ type PlayerEvent = Cooldown : int Timestamp : DateTime } -type HackItem = Item * int * int * int -and ShieldItem = Item * int * int -and FoodItem = Item * (Item -> PlayerData -> PlayerData) -and AccessoryItem = Item * (Item -> PlayerData -> PlayerData) -and ItemType = +type HackItem = { + Id : int + Name : string + Price : int + Power : int + Class : int + Cooldown : int +} + +type ShieldItem = { + Id : int + Name : string + Price : int + Class : int + Cooldown : int +} + +type ItemType = | Hack of power : int * hackClass : int * cooldown : int | Shield of shieldClass : int * cooldown : int | Food of effect : (Item -> PlayerData -> PlayerData) diff --git a/Bot/Games/HackerBattle.fs b/Bot/Games/HackerBattle.fs index f6c3fae..10c5b94 100644 --- a/Bot/Games/HackerBattle.fs +++ b/Bot/Games/HackerBattle.fs @@ -44,7 +44,7 @@ let checkWeaponHasCooldown (weapon : Item) attacker = | None -> Ok attacker let checkHasEmptyHacks attacker = - match Player.getHacks attacker with + match Inventory.getHackItems attacker.Inventory with | [] -> Error $"You currently do not have any Hacks to take 💰$GBT from others. Please go to the <#{GuildEnvironment.channelArmory}> and purchase one." | _ -> Ok attacker @@ -68,36 +68,30 @@ let checkTargetHasFunds target player = | true -> Error $"Looks like the poor bastard has no $GBT... pick a different victim." | false -> Ok player -let calculateDamage ((_,_,hackClass,_) : HackItem) ((_,shieldClass,_) : ShieldItem) = - if hackClass = shieldClass - then Weak - else Strong - let runHackerBattle defender (hack : HackItem) = defender |> Player.removeExpiredActions |> fun p -> p.Events |> Array.choose (fun event -> match event.Type with - | Shielding id -> Armory.getShields |> List.find (fun (item,_,_) -> item.Id = id) |> Some + | Shielding id -> defender.Inventory |> Inventory.getShieldItems |> List.find (fun item -> item.Id = id) |> Some | _ -> None) - |> Array.map (calculateDamage hack) + |> Array.map (fun shield -> if hack.Class = shield.Class then Weak else Strong) |> Array.contains Weak let updateCombatants successfulHack (attacker : PlayerData) (defender : PlayerData) (hack : HackItem) prize = - let (item, power, hackClass, cooldown) = hack let updatePlayer amount attack p = { p with Events = Array.append [| attack |] p.Events ; Bank = max (p.Bank + amount) 0 } let event isDefenderEvent = let hackEvent = { - HackId = item.Id + HackId = hack.Id Adversary = if isDefenderEvent then attacker.basicPlayer else defender.basicPlayer IsInstigator = not isDefenderEvent Success = successfulHack } { Type = Hacking hackEvent Timestamp = DateTime.UtcNow - Cooldown = if isDefenderEvent then int WeaponClass.SameTargetAttackCooldown.TotalMinutes * 1 else cooldown } + Cooldown = if isDefenderEvent then int WeaponClass.SameTargetAttackCooldown.TotalMinutes * 1 else hack.Cooldown } [ DbService.updatePlayer GuildEnvironment.pgDb <| updatePlayer prize (event false) attacker DbService.updatePlayer GuildEnvironment.pgDb <| updatePlayer -prize (event true) defender @@ -108,11 +102,10 @@ let updateCombatants successfulHack (attacker : PlayerData) (defender : PlayerDa let successfulHack (ctx : IDiscordContext) attacker defender (hack : HackItem) = async { - let (item,power,hackClass,cooldown) = hack - let prizeAmount = if power < int defender.Bank then power else int defender.Bank + let prizeAmount = if hack.Power < int defender.Bank then hack.Power else int defender.Bank do! updateCombatants true attacker defender hack (prizeAmount * 1) - let embed = Embeds.responseSuccessfulHack true defender.DiscordId prizeAmount item + let embed = Embeds.responseSuccessfulHack true defender.DiscordId prizeAmount hack do! ctx.FollowUp embed |> Async.AwaitTask let builder = Embeds.eventSuccessfulHack ctx defender prizeAmount @@ -123,9 +116,8 @@ let successfulHack (ctx : IDiscordContext) attacker defender (hack : HackItem) = } let failedHack (ctx : IDiscordContext) attacker defender (hack : HackItem) = - let (item, power, hackClass, cooldown) = hack async { - let lostAmount = if power < int attacker.Bank then power else int attacker.Bank + let lostAmount = if hack.Power < int attacker.Bank then hack.Power else int attacker.Bank let msg = $"Hack failed! {defender.Name} was able to mount a successful defense! You lost {lostAmount} $GBT!" do! sendFollowUpMessage ctx msg @@ -158,7 +150,8 @@ let handleAttack (ctx : IDiscordContext) = executePlayerAction ctx (fun attacker -> async { let tokens = ctx.GetInteractionId().Split("-") let hackId = int tokens.[1] - let item,_,_,_ as hackItem = Armory.getHackById hackId + let hack = Armory.battleItems |> Inventory.findHackById hackId + let hackAsItem = Inventory.hackToItem hack let resultId , targetId = UInt64.TryParse tokens.[2] let! resultTarget = DbService.tryFindPlayer GuildEnvironment.pgDb targetId @@ -167,21 +160,21 @@ let handleAttack (ctx : IDiscordContext) = do! attacker |> Player.removeExpiredActions |> checkAlreadyHackedTarget defender - >>= checkPlayerOwnsWeapon item - >>= checkWeaponHasCooldown item + >>= checkPlayerOwnsWeapon hackAsItem + >>= checkWeaponHasCooldown hackAsItem |> function | Ok atkr -> - runHackerBattle defender hackItem + runHackerBattle defender hack |> function - | false -> successfulHack ctx atkr defender hackItem - | true -> failedHack ctx attacker defender hackItem + | false -> successfulHack ctx atkr defender hack + | true -> failedHack ctx attacker defender hack | Error msg -> Messaging.sendFollowUpMessage ctx msg | _ -> do! Messaging.sendFollowUpMessage ctx "Error occurred processing attack" }) let defend (ctx : IDiscordContext) = executePlayerAction ctx (fun player -> async { - if Player.getShields player |> List.length > 0 then + if player.Inventory |> Inventory.filterByShields |> List.length > 0 then let p = Player.removeExpiredActions player let embed = Embeds.pickDefense "Defend" p false do! ctx.FollowUp embed |> Async.AwaitTask @@ -194,18 +187,19 @@ let handleDefense (ctx : IDiscordContext) = executePlayerAction ctx (fun player -> async { let tokens = ctx.GetInteractionId().Split("-") let shieldId = int tokens.[1] - let item, shieldClass, cooldown as shield = Armory.getShields |> List.find (fun (item,_,_) -> item.Id = shieldId) + let shield = Armory.battleItems |> Inventory.findShieldById shieldId + let shieldAsItem = Inventory.shieldToItem shield do! player - |> checkPlayerOwnsWeapon item + |> checkPlayerOwnsWeapon shieldAsItem >>= checkPlayerHasShieldSlotsAvailable - >>= checkWeaponHasCooldown item + >>= checkWeaponHasCooldown shieldAsItem |> handleResultWithResponse ctx (fun p -> async { let embed = Embeds.responseCreatedShield shield do! ctx.FollowUp embed |> Async.AwaitTask let defense = { Type = Shielding shieldId - Cooldown = cooldown + Cooldown = shield.Cooldown Timestamp = DateTime.UtcNow } do! DbService.updatePlayer GuildEnvironment.pgDb p diff --git a/Bot/Games/Store.fs b/Bot/Games/Store.fs index 26caf93..ce5dde8 100644 --- a/Bot/Games/Store.fs +++ b/Bot/Games/Store.fs @@ -1,5 +1,6 @@ module Degenz.Store +open System open System.Threading.Tasks open DSharpPlus.Entities open DSharpPlus @@ -9,6 +10,58 @@ open Degenz open Degenz.Messaging open Degenz.PlayerInteractions +let getBuyItemsEmbed (playerInventory : Inventory) (storeInventory : Inventory) = + let embeds , buttons = + storeInventory + |> List.map (fun item -> + let embed = DiscordEmbedBuilder() + match item.Type with + | Hack(power,_,cooldown) -> + embed.AddField($"$GBT Reward |", string power, true) + .AddField("Cooldown |", $"{TimeSpan.FromMinutes(int cooldown).Minutes} minutes", true) + .WithThumbnail(Embeds.getItemIcon item.Id) + |> ignore + | Shield(shieldClass,cooldown) -> + embed.AddField($"Strong against |", WeaponClass.getGoodAgainst shieldClass |> snd |> string, true) +// .AddField($"Defensive Strength |", string item.Power, true) + .AddField("Active For |", $"{TimeSpan.FromMinutes(int cooldown).Hours} hours", true) + .WithThumbnail(Embeds.getItemIcon item.Id) + |> ignore + | _ -> () + embed + .AddField("Price 💰", (if item.Price = 0 then "Free" else $"{item.Price} $GBT"), true) + .WithColor(WeaponClass.getClassEmbedColor item) + .WithTitle($"{item.Name}") + |> ignore + let button = + if playerInventory |> List.exists (fun i -> i.Id = item.Id) + then DiscordButtonComponent(WeaponClass.getClassButtonColor item, $"Buy-{item.Id}", $"Own {item.Name}", true) + else DiscordButtonComponent(WeaponClass.getClassButtonColor item, $"Buy-{item.Id}", $"Buy {item.Name}") + ( embed.Build() , button :> DiscordComponent )) + |> List.unzip + + DiscordFollowupMessageBuilder() + .AddEmbeds(embeds) + .AddComponents(buttons) + .AsEphemeral(true) + +let getSellEmbed (items : Item list) = + let embeds , buttons = + items + |> List.map (fun item -> + DiscordEmbedBuilder() + .AddField("Sell For 💰", $"{item.Price} $GBT", true) + .WithTitle($"{item.Name}") + .WithColor(WeaponClass.getClassEmbedColor item) + .Build() + , DiscordButtonComponent(WeaponClass.getClassButtonColor item, $"Sell-{id}", $"Sell {item.Name}") :> DiscordComponent) + |> List.unzip + + DiscordFollowupMessageBuilder() + .AddEmbeds(embeds) + .AddComponents(buttons) + .AsEphemeral(true) + let checkHasSufficientFunds (item : Item) player = if player.Bank - item.Price >= 0 then Ok player @@ -31,15 +84,15 @@ let checkHasItemsInArsenal itemType items player = let buy getItems (ctx : IDiscordContext) = executePlayerAction ctx (fun player -> async { - let itemStore = Embeds.getBuyItemsEmbed (getItems player.Inventory) (getItems Armory.battleItems) + let itemStore = getBuyItemsEmbed (getItems player.Inventory) (getItems Armory.battleItems) do! ctx.FollowUp itemStore |> Async.AwaitTask }) let sell itemType getItems (ctx : IDiscordContext) = executePlayerAction ctx (fun player -> async { - let items = getItems player + let items = getItems player.Inventory match checkHasItemsInArsenal itemType items player with - | Ok _ -> let itemStore = Embeds.getSellEmbed items + | Ok _ -> let itemStore = getSellEmbed items do! ctx.FollowUp(itemStore) |> Async.AwaitTask | Error e -> do! sendFollowUpMessage ctx e }) @@ -114,8 +167,8 @@ type Store() = member this.BuyShield (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (buy Armory.getShieldItems) [] - member this.SellHack (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (sell "Hacks" Player.getHackItems) + member this.SellHack (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (sell "Hacks" Inventory.filterByHacks) [] - member this.SellShield (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (sell "Shields" Player.getShieldItems) + member this.SellShield (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (sell "Shields" Inventory.filterByShields) diff --git a/Bot/Games/Trainer.fs b/Bot/Games/Trainer.fs index a0aa6c1..e9873d7 100644 --- a/Bot/Games/Trainer.fs +++ b/Bot/Games/Trainer.fs @@ -9,20 +9,20 @@ open Degenz.Messaging let trainerAchievement = "FINISHED_TRAINER" let Sensei = { Id = GuildEnvironment.botIdHackerBattle ; Name = "Sensei" } -let defaultHackItem, hackPower, hackClass, hackCooldown as defaultHack = Armory.getHackById (int ItemId.Virus) -let defaultShieldItem, shieldClass, shieldCooldown as defaultShield = Armory.getShieldById (int ItemId.Firewall) +let defaultHack = Armory.battleItems |> Inventory.findHackById (int ItemId.Virus) +let defaultShield = Armory.battleItems |> Inventory.findShieldById (int ItemId.Firewall) let TrainerEvents = [| { Timestamp = System.DateTime.UtcNow - Cooldown = 2 + Cooldown = defaultHack.Cooldown Type = Hacking { Adversary = Sensei Success = true IsInstigator = true - HackId = defaultHackItem.Id } } + HackId = defaultHack.Id } } { Timestamp = System.DateTime.UtcNow - Cooldown = shieldCooldown - Type = Shielding defaultShieldItem.Id } + Cooldown = defaultShield.Cooldown + Type = Shielding defaultShield.Id } |] let sendInitialEmbed (client : DiscordClient) = @@ -52,7 +52,7 @@ let handleTrainerStep1 (ctx : IDiscordContext) = |> Async.AwaitTask let msg = "Beautopia© is a dangerous place... quick, put up a SHIELD 🛡 before another Degen hacks you, and takes your 💰$GBT.\n\n" + "To enable it, you need to run the `/shield` slash command.\n\n" - + $"Type the `/shield` command now, then select - `{defaultShieldItem.Name}`\n" + + $"Type the `/shield` command now, then select - `{defaultShield.Name}`\n" let builder = DiscordInteractionResponseBuilder() .WithContent(msg) @@ -66,7 +66,7 @@ let defend (ctx : IDiscordContext) = do! Messaging.defer ctx let m = ctx.GetDiscordMember() let name = if System.String.IsNullOrEmpty m.Nickname then m.DisplayName else m.Nickname - let embed = Embeds.pickDefense "Trainer-2" { PlayerData.empty with Inventory = [ defaultShieldItem ] ; Name = name } true + let embed = Embeds.pickDefense "Trainer-2" { PlayerData.empty with Inventory = [ Inventory.shieldToItem defaultShield ] ; Name = name } true do! ctx.FollowUp(embed) |> Async.AwaitTask } |> Async.StartAsTask :> Task @@ -88,11 +88,11 @@ let handleDefense (ctx : IDiscordContext) = let embed = Embeds.responseCreatedShield defaultShield do! ctx.FollowUp embed |> Async.AwaitTask do! Async.Sleep 4000 - do! sendMessage' $"Ok, good, let me make sure that worked.\n\nI'll try to **hack** you now with **{defaultHackItem.Name}**" + do! sendMessage' $"Ok, good, let me make sure that worked.\n\nI'll try to **hack** you now with **{defaultHack.Name}**" do! Async.Sleep 5000 do! sendMessage' $"❌ HACKING FAILED!\n\n{playerName} defended hack from <@{Sensei.Id}>!" do! Async.Sleep 4000 - do! sendFollowUpMessageWithButton ctx (handleDefenseMsg defaultHackItem.Name) + do! sendFollowUpMessageWithButton ctx (handleDefenseMsg defaultHack.Name) } |> Async.StartAsTask :> Task let handleTrainerStep3 (ctx : IDiscordContext) = @@ -103,7 +103,7 @@ let handleTrainerStep3 (ctx : IDiscordContext) = .WithContent ( "Now let’s **HACK** 💻... I want you to **HACK ME**!\n\n" + "To **hack**, you need to run the `/hack` slash command.\n" - + $"Type the `/hack` command now, then choose me - <@{Sensei.Id}> as your target, and select `{defaultHackItem.Name}`") + + $"Type the `/hack` command now, then choose me - <@{Sensei.Id}> as your target, and select `{defaultHack.Name}`") do! ctx.Respond(InteractionResponseType.ChannelMessageWithSource, builder) |> Async.AwaitTask } |> Async.StartAsTask :> Task @@ -115,8 +115,9 @@ let hack (target : DiscordUser) (ctx : IDiscordContext) = let isRightTarget = target.Id = Sensei.Id match isRightTarget with | true -> + let player = { PlayerData.empty with Inventory = [ Inventory.hackToItem defaultHack ] } let bot = { PlayerData.empty with DiscordId = Sensei.Id ; Name = Sensei.Name } - let embed = Embeds.pickHack "Trainer-4" { PlayerData.empty with Inventory = [ defaultHackItem ] } bot true + let embed = Embeds.pickHack "Trainer-4" player bot true do! ctx.FollowUp(embed) |> Async.AwaitTask | false -> @@ -132,7 +133,7 @@ let handleHack (ctx : IDiscordContext) = PlayerInteractions.executePlayerAction ctx (fun player -> async { let sendMessage' = sendFollowUpMessage ctx do! Async.Sleep 1000 - let embed = Embeds.responseSuccessfulHack false Sensei.Id hackPower defaultHackItem + let embed = Embeds.responseSuccessfulHack false Sensei.Id defaultHack.Power defaultHack do! ctx.FollowUp(embed) |> Async.AwaitTask do! Async.Sleep 4000 do! sendMessage' @@ -150,7 +151,7 @@ let handleHack (ctx : IDiscordContext) = do! DbService.addAchievement GuildEnvironment.pgDb player.DiscordId trainerAchievement |> Async.Ignore - sb.Append($"I'm going to gift you a hack,`{defaultHackItem.Name}` and a shield, `{defaultShieldItem.Name}`") |> ignore + sb.Append($"I'm going to gift you a hack,`{defaultHack.Name}` and a shield, `{defaultShield.Name}`") |> 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 @@ -165,12 +166,15 @@ let handleHack (ctx : IDiscordContext) = let handleArsenal (ctx : IDiscordContext) = PlayerInteractions.executePlayerAction ctx (fun player -> async { - let hasStockWeapons = Player.getHackItems player |> List.exists (fun item -> item.Id = defaultHackItem.Id) + let hasStockWeapons = + player.Inventory + |> List.choose (fun item -> if item.Id = defaultHack.Id || item.Id = defaultShield.Id then Some item else None) + |> List.length > 0 let updatedPlayer = if not hasStockWeapons then { Player.removeExpiredActions player with Events = TrainerEvents |> Array.append player.Events - Inventory = defaultHackItem::defaultShieldItem::player.Inventory + Inventory = Inventory.hackToItem defaultHack::Inventory.shieldToItem defaultShield::player.Inventory } else Player.removeExpiredActions player @@ -187,7 +191,7 @@ let handleArsenal (ctx : IDiscordContext) = let! completed = DbService.checkHasAchievement GuildEnvironment.pgDb player.DiscordId trainerAchievement if not completed then do! Async.Sleep 3000 - let rewards = [ $"{defaultHackItem.Name} Hack" ; $"{defaultShieldItem.Name} Shield" ] + let rewards = [ $"{defaultHack.Name} Hack" ; $"{defaultShield.Name} Shield" ] let embed = Embeds.getAchievementEmbed rewards "You completed the Training Dojo and collected loot." trainerAchievement do! ctx.FollowUp(embed) |> Async.AwaitTask do! Async.Sleep 2000 From 7a4bf025632f243e2d7a83b77a83b1169ec2c5cd Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sun, 27 Feb 2022 22:59:26 +0700 Subject: [PATCH 06/28] Remove armory functions so it's just inventory. Start on stats and new items --- Bot/DbService.fs | 4 +-- Bot/GameHelpers.fs | 58 +++++++++++++-------------------------- Bot/GameTypes.fs | 35 ++++++++++++----------- Bot/Games/HackerBattle.fs | 4 +-- Bot/Games/Store.fs | 10 +++---- Bot/Games/Trainer.fs | 4 +-- 6 files changed, 49 insertions(+), 66 deletions(-) diff --git a/Bot/DbService.fs b/Bot/DbService.fs index 081b8d5..630e936 100644 --- a/Bot/DbService.fs +++ b/Bot/DbService.fs @@ -15,9 +15,9 @@ type User = { let mapBack user : PlayerData = { DiscordId = user.DiscordId Name = user.Name - Inventory = user.Inventory |> List.choose (fun id -> Armory.battleItems |> List.tryFind (fun item -> item.Id = id)) + Inventory = user.Inventory |> List.choose (fun id -> Armory.weapons |> List.tryFind (fun item -> item.Id = id)) Events = [||] - Stats = [ { Id = StatId.Strength ; Amount = user.Strength } ] + Stats = [ { Id = StatId.Strength ; Amount = user.Strength ; LastRead = DateTime.UtcNow} ] Bank = user.Bank } diff --git a/Bot/GameHelpers.fs b/Bot/GameHelpers.fs index 2218b58..1524e73 100644 --- a/Bot/GameHelpers.fs +++ b/Bot/GameHelpers.fs @@ -1,32 +1,17 @@ namespace Degenz +open System open DSharpPlus open DSharpPlus.Entities open Newtonsoft.Json module Armory = - let battleItems = + let weapons = let file = System.IO.File.ReadAllText("Items.json") // let file = System.IO.File.ReadAllText("Bot/Items.json") JsonConvert.DeserializeObject(file) |> Array.toList - let hacks = battleItems |> List.filter (fun item -> match item.Type with Hack _ -> true | _ -> false) - let shields = battleItems |> List.filter (fun item -> match item.Type with Shield _ -> true | _ -> false) - - let getItem itemId = battleItems |> List.find (fun item -> item.Id = itemId) - - let getHackItems inventory = - inventory - |> List.filter (fun item -> match item.Type with Hack _ -> true | _ -> false) - |> List.sortBy (fun item -> item.Id) - let getShieldItems inventory = - inventory - |> List.filter (fun item -> match item.Type with Shield _ -> true | _ -> false) - |> List.sortBy (fun item -> item.Id) - let getHackById id inventory = inventory |> getHackItems |> List.find (fun item -> item.Id = id) - let getShieldById id inventory = inventory |> getShieldItems |> List.find (fun item -> item.Id = id) - module Inventory = let itemToHack item power hackClass cooldown = { Id = item.Id @@ -70,6 +55,8 @@ module Inventory = inventory |> List.choose (fun item -> match item.Type with Shield (cl,co) -> Some (itemToShield item cl co) | _ -> None) |> List.sortBy (fun item -> item.Id) + let findItemById id inventory = inventory |> List.find (fun item -> item.Id = id) + let tryFindItemById id inventory = inventory |> List.tryFind (fun item -> item.Id = id) let findHackById id inventory = inventory |> getHackItems |> List.pick (fun item -> if item.Id = id then Some item else None) let findShieldById id inventory = @@ -126,6 +113,19 @@ module Player = |> Option.map (fun stat -> stat.Amount) |> Option.defaultValue 0 +module PlayerStats = + let Strength = { Id = StatId.Strength ; BaseDecayRate = 5.0f ; BaseMinMax = Range(0, 100) } + let Focus = { Id = StatId.Focus ; BaseDecayRate = 5.0f ; BaseMinMax = Range(0, 100) } + let Luck = { Id = StatId.Luck ; BaseDecayRate = 5.0f ; BaseMinMax = Range(0, 100) } + let Charisma = { Id = StatId.Charisma ; BaseDecayRate = 5.0f ; BaseMinMax = Range(0, 100) } + + let stats = [| Strength ; Focus ; Luck ; Charisma |] + let statConsumableMap = + [ ( StatId.Strength , 12 ) + ( StatId.Focus , 13 ) + ( StatId.Luck , 14 ) + ( StatId.Charisma , 15 ) ] + module Arsenal = let battleItemFormat (items : Item list) = match items with @@ -140,11 +140,11 @@ module Arsenal = |> Array.map (fun act -> match act.Type with | Hacking h -> - let item = Armory.getItem h.HackId + let item = Armory.weapons |> Inventory.findHackById h.HackId let cooldown = Messaging.getTimeText false WeaponClass.SameTargetAttackCooldown act.Timestamp $"Hacked {h.Adversary.Name} with {item.Name} {cooldown} ago" | Shielding id -> - let item = Armory.getItem id + let item = Armory.weapons |> Inventory.findHackById id let cooldown = Messaging.getTimeText true (System.TimeSpan.FromMinutes(int act.Cooldown)) act.Timestamp $"{item.Name} Shield active for {cooldown}" | _ -> "") @@ -157,23 +157,3 @@ module Arsenal = **Shields:** {Inventory.filterByShields p.Inventory |> battleItemFormat}\n **Hack Attacks:**\n{ hacks |> Array.take (min hacks.Length 10) |> actionFormat}\n **Active Shields:**\n{Player.getShieldEvents p |> actionFormat}" - -module Items = - let mapHack fn inventory = - inventory - |> List.choose (fun item -> - match item.Type with - | Hack(power, hackClass, cooldown) -> Some <| fn item power hackClass cooldown - | _ -> None) - let mapShield fn inventory = - inventory - |> List.choose (fun item -> - match item.Type with - | Shield(hackClass, cooldown) -> Some <| fn item hackClass cooldown - | _ -> None) - let doShields fn inventory = - inventory - |> List.iter (fun item -> - match item.Type with - | Shield(hackClass, cooldown) -> fn item hackClass cooldown - | _ -> ()) diff --git a/Bot/GameTypes.fs b/Bot/GameTypes.fs index b9e9454..717f42a 100644 --- a/Bot/GameTypes.fs +++ b/Bot/GameTypes.fs @@ -32,26 +32,13 @@ type Stat = { type ActiveStat = { Id : StatId Amount : int + LastRead : DateTime } -module PlayerStats = - let Strength = { Id = StatId.Strength ; BaseDecayRate = 5.0f ; BaseMinMax = Range(0, 100) } - let Focus = { Id = StatId.Focus ; BaseDecayRate = 5.0f ; BaseMinMax = Range(0, 100) } - let Luck = { Id = StatId.Luck ; BaseDecayRate = 5.0f ; BaseMinMax = Range(0, 100) } - let Charisma = { Id = StatId.Charisma ; BaseDecayRate = 5.0f ; BaseMinMax = Range(0, 100) } - - let stats = [| Strength ; Focus ; Luck ; Charisma |] - let statConsumableMap = - [ ( StatId.Strength , 12 ) - ( StatId.Focus , 13 ) - ( StatId.Luck , 14 ) - ( StatId.Charisma , 15 ) ] - type HackResult = | Strong | Weak -[] type DiscordPlayer = { Id: uint64; Name: string } with static member empty = { Id = 0uL ; Name = "None" } @@ -90,11 +77,27 @@ type ShieldItem = { Cooldown : int } +type FoodItem = { + Id : int + Name : string + Price : int + TargetStat : StatId + BoostAmount : int +} + +type AccessoryItem = { + Id : int + Name : string + Price : int + TargetStat : StatId + PassiveBoost : int +} + type ItemType = | Hack of power : int * hackClass : int * cooldown : int | Shield of shieldClass : int * cooldown : int - | Food of effect : (Item -> PlayerData -> PlayerData) - | Accessory of effect : (Item -> PlayerData -> PlayerData) + | Food of targetStat : StatId * boostAmount : int + | Accessory of targetStat : StatId * passiveBoost : int and Item = { Id : int Name : string diff --git a/Bot/Games/HackerBattle.fs b/Bot/Games/HackerBattle.fs index 10c5b94..31ccd1e 100644 --- a/Bot/Games/HackerBattle.fs +++ b/Bot/Games/HackerBattle.fs @@ -150,7 +150,7 @@ let handleAttack (ctx : IDiscordContext) = executePlayerAction ctx (fun attacker -> async { let tokens = ctx.GetInteractionId().Split("-") let hackId = int tokens.[1] - let hack = Armory.battleItems |> Inventory.findHackById hackId + let hack = Armory.weapons |> Inventory.findHackById hackId let hackAsItem = Inventory.hackToItem hack let resultId , targetId = UInt64.TryParse tokens.[2] let! resultTarget = DbService.tryFindPlayer GuildEnvironment.pgDb targetId @@ -187,7 +187,7 @@ let handleDefense (ctx : IDiscordContext) = executePlayerAction ctx (fun player -> async { let tokens = ctx.GetInteractionId().Split("-") let shieldId = int tokens.[1] - let shield = Armory.battleItems |> Inventory.findShieldById shieldId + let shield = Armory.weapons |> Inventory.findShieldById shieldId let shieldAsItem = Inventory.shieldToItem shield do! player diff --git a/Bot/Games/Store.fs b/Bot/Games/Store.fs index ce5dde8..6b9029b 100644 --- a/Bot/Games/Store.fs +++ b/Bot/Games/Store.fs @@ -84,7 +84,7 @@ let checkHasItemsInArsenal itemType items player = let buy getItems (ctx : IDiscordContext) = executePlayerAction ctx (fun player -> async { - let itemStore = getBuyItemsEmbed (getItems player.Inventory) (getItems Armory.battleItems) + let itemStore = getBuyItemsEmbed (getItems player.Inventory) (getItems Armory.weapons) do! ctx.FollowUp itemStore |> Async.AwaitTask }) @@ -100,7 +100,7 @@ let sell itemType getItems (ctx : IDiscordContext) = // TODO: When you buy a shield, prompt the user to activate it let handleBuyItem (ctx : IDiscordContext) itemId = executePlayerAction ctx (fun player -> async { - let item = Armory.getItem itemId + let item = Armory.weapons |> Inventory.findItemById itemId do! player |> checkHasSufficientFunds item >>= checkAlreadyOwnsItem item @@ -114,7 +114,7 @@ let handleBuyItem (ctx : IDiscordContext) itemId = let handleSell (ctx : IDiscordContext) itemId = executePlayerAction ctx (fun player -> async { - let item = Armory.getItem itemId + let item = Armory.weapons |> Inventory.findItemById itemId do! player |> checkSoldItemAlready item @@ -161,10 +161,10 @@ type Store() = } [] - member _.BuyHack (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (buy Armory.getHackItems) + member _.BuyHack (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (buy Inventory.filterByHacks) [] - member this.BuyShield (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (buy Armory.getShieldItems) + member this.BuyShield (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (buy Inventory.filterByShields) [] member this.SellHack (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (sell "Hacks" Inventory.filterByHacks) diff --git a/Bot/Games/Trainer.fs b/Bot/Games/Trainer.fs index e9873d7..24df899 100644 --- a/Bot/Games/Trainer.fs +++ b/Bot/Games/Trainer.fs @@ -9,8 +9,8 @@ open Degenz.Messaging let trainerAchievement = "FINISHED_TRAINER" let Sensei = { Id = GuildEnvironment.botIdHackerBattle ; Name = "Sensei" } -let defaultHack = Armory.battleItems |> Inventory.findHackById (int ItemId.Virus) -let defaultShield = Armory.battleItems |> Inventory.findShieldById (int ItemId.Firewall) +let defaultHack = Armory.weapons |> Inventory.findHackById (int ItemId.Virus) +let defaultShield = Armory.weapons |> Inventory.findShieldById (int ItemId.Firewall) let TrainerEvents = [| { Timestamp = System.DateTime.UtcNow From 45fab5ca82c56d4a69db4ccf079860c2d33881aa Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sun, 27 Feb 2022 23:13:23 +0700 Subject: [PATCH 07/28] Stat decay --- Bot/GameHelpers.fs | 9 ++++++++- Bot/GameTypes.fs | 7 ++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Bot/GameHelpers.fs b/Bot/GameHelpers.fs index 1524e73..2cd13ef 100644 --- a/Bot/GameHelpers.fs +++ b/Bot/GameHelpers.fs @@ -119,7 +119,14 @@ module PlayerStats = let Luck = { Id = StatId.Luck ; BaseDecayRate = 5.0f ; BaseMinMax = Range(0, 100) } let Charisma = { Id = StatId.Charisma ; BaseDecayRate = 5.0f ; BaseMinMax = Range(0, 100) } - let stats = [| Strength ; Focus ; Luck ; Charisma |] + let stats = [ Strength ; Focus ; Luck ; Charisma ] + + let calculateStatDecay (stat : PlayerStat) = + let statConfig = stats |> List.find (fun s -> s.Id = stat.Id) + let hoursElapsed = (DateTime.UtcNow - stat.LastRead).Hours + let totalDecay = hoursElapsed * int statConfig.BaseDecayRate + { stat with Amount = max stat.ModMinMax.Start.Value (stat.Amount - totalDecay) ; LastRead = DateTime.UtcNow } + let statConsumableMap = [ ( StatId.Strength , 12 ) ( StatId.Focus , 13 ) diff --git a/Bot/GameTypes.fs b/Bot/GameTypes.fs index 717f42a..4b3e1a2 100644 --- a/Bot/GameTypes.fs +++ b/Bot/GameTypes.fs @@ -23,15 +23,16 @@ type StatId = | Luck = 2 | Charisma = 3 -type Stat = { +type StatConfig = { Id : StatId BaseDecayRate : single BaseMinMax : Range } -type ActiveStat = { +type PlayerStat = { Id : StatId Amount : int + ModMinMax : Range LastRead : DateTime } @@ -110,7 +111,7 @@ and PlayerData = { Name : string Inventory : Inventory Events : PlayerEvent array - Stats : ActiveStat list + Stats : PlayerStat list Bank : int } // Achievements : string array From 53f60bcf86f4fb061af81e80f30bfc29a977b9f6 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sun, 27 Feb 2022 23:37:47 +0700 Subject: [PATCH 08/28] Reshape stat types --- Bot/DbService.fs | 4 ++-- Bot/GameHelpers.fs | 20 +++++++------------- Bot/GameTypes.fs | 16 +++++++++++++--- Bot/Games/Thief.fs | 10 +++------- 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/Bot/DbService.fs b/Bot/DbService.fs index 630e936..21527c6 100644 --- a/Bot/DbService.fs +++ b/Bot/DbService.fs @@ -17,7 +17,7 @@ let mapBack user : PlayerData = Name = user.Name Inventory = user.Inventory |> List.choose (fun id -> Armory.weapons |> List.tryFind (fun item -> item.Id = id)) Events = [||] - Stats = [ { Id = StatId.Strength ; Amount = user.Strength ; LastRead = DateTime.UtcNow} ] + Stats = { Stats.empty with Strength = { Id = StatId.Strength ; ModMinMax = Range(0, 100) ; Amount = user.Strength; LastRead = DateTime.UtcNow } } Bank = user.Bank } @@ -90,7 +90,7 @@ let updatePlayer connStr (player : PlayerData) = |> Sql.parameters [ "did", Sql.string (string player.DiscordId) "gbt", Sql.int (int player.Bank) - "str", Sql.int (player |> Player.getStat StatId.Strength |> int) + "str", Sql.int (player.Stats.Strength.Amount |> int) "inv", Sql.intArray (player.Inventory |> Array.ofList |> Array.map (fun item -> item.Id)) ] |> Sql.query """ diff --git a/Bot/GameHelpers.fs b/Bot/GameHelpers.fs index 2cd13ef..064a630 100644 --- a/Bot/GameHelpers.fs +++ b/Bot/GameHelpers.fs @@ -67,7 +67,6 @@ module Inventory = inventory |> getShieldItems |> List.tryFind (fun item -> item.Id = id) module WeaponClass = - // TODO: Find a different place to put this let SameTargetAttackCooldown = System.TimeSpan.FromHours(1) let getClassButtonColor item = @@ -107,25 +106,20 @@ module Player = let modifyBank (player : PlayerData) amount = { player with Bank = max (player.Bank + amount) 0 } - let getStat statId player = - player.Stats - |> List.tryFind (fun stat -> statId = stat.Id) - |> Option.map (fun stat -> stat.Amount) - |> Option.defaultValue 0 - module PlayerStats = - let Strength = { Id = StatId.Strength ; BaseDecayRate = 5.0f ; BaseMinMax = Range(0, 100) } - let Focus = { Id = StatId.Focus ; BaseDecayRate = 5.0f ; BaseMinMax = Range(0, 100) } - let Luck = { Id = StatId.Luck ; BaseDecayRate = 5.0f ; BaseMinMax = Range(0, 100) } - let Charisma = { Id = StatId.Charisma ; BaseDecayRate = 5.0f ; BaseMinMax = Range(0, 100) } + // 4.17f would go from 100 to 0 in roughly 24 hours + let Strength = { Id = StatId.Strength ; BaseDecayRate = 4.17 ; BaseMinMax = Range(0, 100) } + let Focus = { Id = StatId.Focus ; BaseDecayRate = 4.17 ; BaseMinMax = Range(0, 100) } + let Luck = { Id = StatId.Luck ; BaseDecayRate = 4.17 ; BaseMinMax = Range(0, 100) } + let Charisma = { Id = StatId.Charisma ; BaseDecayRate = 4.17 ; BaseMinMax = Range(0, 100) } let stats = [ Strength ; Focus ; Luck ; Charisma ] let calculateStatDecay (stat : PlayerStat) = let statConfig = stats |> List.find (fun s -> s.Id = stat.Id) let hoursElapsed = (DateTime.UtcNow - stat.LastRead).Hours - let totalDecay = hoursElapsed * int statConfig.BaseDecayRate - { stat with Amount = max stat.ModMinMax.Start.Value (stat.Amount - totalDecay) ; LastRead = DateTime.UtcNow } + let totalDecay = float hoursElapsed * statConfig.BaseDecayRate + { stat with Amount = max stat.ModMinMax.Start.Value (stat.Amount - int totalDecay) ; LastRead = DateTime.UtcNow } let statConsumableMap = [ ( StatId.Strength , 12 ) diff --git a/Bot/GameTypes.fs b/Bot/GameTypes.fs index 4b3e1a2..4aa8cff 100644 --- a/Bot/GameTypes.fs +++ b/Bot/GameTypes.fs @@ -2,6 +2,7 @@ module Degenz.Types open System +open Degenz [] type mins @@ -25,7 +26,7 @@ type StatId = type StatConfig = { Id : StatId - BaseDecayRate : single + BaseDecayRate : float BaseMinMax : Range } @@ -35,6 +36,15 @@ type PlayerStat = { ModMinMax : Range LastRead : DateTime } +with static member empty = { Id = StatId.Strength ; Amount = 0 ; ModMinMax = Range(0, 100) ; LastRead = DateTime.UtcNow} + +type Stats = { + Strength : PlayerStat + Focus : PlayerStat + Luck : PlayerStat + Charisma : PlayerStat +} +with static member empty = { Strength = PlayerStat.empty ; Focus = PlayerStat.empty ; Luck = PlayerStat.empty ; Charisma = PlayerStat.empty } type HackResult = | Strong @@ -111,7 +121,7 @@ and PlayerData = { Name : string Inventory : Inventory Events : PlayerEvent array - Stats : PlayerStat list + Stats : Stats Bank : int } // Achievements : string array @@ -122,7 +132,7 @@ with member this.basicPlayer = { Id = this.DiscordId ; Name = this.Name } Name = "None" Inventory = [] Events = [||] - Stats = [] + Stats = Stats.empty // Achievements = [||] // XP = 0 Bank = 0 } diff --git a/Bot/Games/Thief.fs b/Bot/Games/Thief.fs index b94ed29..5a7264a 100644 --- a/Bot/Games/Thief.fs +++ b/Bot/Games/Thief.fs @@ -128,9 +128,7 @@ let steal target amount (ctx : IDiscordContext) = >>= checkPrizeRequestZero amount |> handleResultWithResponse ctx (fun _ -> async { let cappedPrize , winPercentage , wasCapped = -// calculateWinPercentage amount (int victim.Bank) thief.Stats.Strength victim.Stats.Strength - // TODO: Readd stats - calculateWinPercentage amount (int victim.Bank) 0 0 + calculateWinPercentage amount (int victim.Bank) thief.Stats.Strength.Amount victim.Stats.Strength.Amount let chance = int (winPercentage * 100.0) let buttons = @@ -140,8 +138,7 @@ let steal target amount (ctx : IDiscordContext) = let cappedMsg = if wasCapped then $"They only have {cappedPrize} $GBT though... " else "" let strengthMsg = - // TODO: Readd stats - match 0 - 0 with + match thief.Stats.Strength.Amount - victim.Stats.Strength.Amount with | diff when diff < -50 -> "much stronger" | diff when diff < 0 -> "stronger" | diff when diff < 50 -> "weaker" @@ -164,8 +161,7 @@ let handleSteal (ctx : IDiscordContext) = let targetId = uint64 tokens.[2] let targetName = tokens.[3] let amount = int tokens.[4] - // TODO: Readd stats - let prize , winPercentage , _ = calculateWinPercentage amount (int victim.Bank) 0 0 + let prize , winPercentage , _ = calculateWinPercentage amount (int victim.Bank) thief.Stats.Strength.Amount victim.Stats.Strength.Amount let prize = int prize * 1 let rand = Random(Guid.NewGuid().GetHashCode()) From 05cdb98beec2935b00fe92ae82d35eb2d90c24b6 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Tue, 1 Mar 2022 13:14:32 +0700 Subject: [PATCH 09/28] Just read the stats from the user table. Remove dbconnectione everywhere --- Bot/DbService.fs | 141 +++++++++++++++++++++++--------------- Bot/GameHelpers.fs | 45 ++++++------ Bot/GameTypes.fs | 24 ++++--- Bot/Games/HackerBattle.fs | 18 ++--- Bot/Games/SlotMachine.fs | 4 +- Bot/Games/Store.fs | 6 +- Bot/Games/Thief.fs | 14 ++-- Bot/Games/Trainer.fs | 12 ++-- Bot/GuildEnvironment.fs | 2 +- Bot/PlayerInteractions.fs | 10 +-- 10 files changed, 156 insertions(+), 120 deletions(-) diff --git a/Bot/DbService.fs b/Bot/DbService.fs index 21527c6..0402e58 100644 --- a/Bot/DbService.fs +++ b/Bot/DbService.fs @@ -4,27 +4,23 @@ open System open Npgsql.FSharp open Degenz +let connStr = GuildEnvironment.connectionString + type User = { Name : string DiscordId : uint64 Bank : int - Strength : int Inventory : int list + Strength : int + Focus : int + Charisma : int + Luck : int } -let mapBack user : PlayerData = - { DiscordId = user.DiscordId - Name = user.Name - Inventory = user.Inventory |> List.choose (fun id -> Armory.weapons |> List.tryFind (fun item -> item.Id = id)) - Events = [||] - Stats = { Stats.empty with Strength = { Id = StatId.Strength ; ModMinMax = Range(0, 100) ; Amount = user.Strength; LastRead = DateTime.UtcNow } } - Bank = user.Bank -} - -let getPlayerEvents connStr (player : PlayerData) = +let getPlayerEvents (did : uint64) = connStr |> Sql.connect - |> Sql.parameters [ "did", Sql.string (string player.DiscordId) ] + |> Sql.parameters [ "did", Sql.string (string did) ] |> Sql.query """ WITH usr AS (SELECT id FROM "user" WHERE discord_id = @did) SELECT event_type, success, is_instigator, item_id, cooldown, adversary_id, adversary_name, created_at @@ -48,59 +44,92 @@ let getPlayerEvents connStr (player : PlayerData) = ) |> Async.AwaitTask -let tryFindPlayer connStr (discordId : uint64) = - async { - try - let! user = -// use cert = new X509Certificate2("~/Downloads/ca-certificate.crt") -// (Uri connStr) -// |> Sql.fromUriToConfig -// |> Sql.requireSslMode -// |> Sql.formatConnectionString -// |> Sql.clientCertificate cert - connStr - |> Sql.connect - |> Sql.parameters [ "did", Sql.string (string discordId) ] - |> Sql.query """ - SELECT discord_id, display_name, gbt, strength, inventory FROM "user" WHERE discord_id = @did - """ - |> Sql.executeAsync (fun read -> - { - DiscordId = read.string "discord_id" |> uint64 - Name = read.string "display_name" - Bank = read.int "gbt" * 1 - Strength = read.int "strength" - Inventory = read.intArray "inventory" |> Array.toList - }) - |> Async.AwaitTask - match List.tryHead user with - | None -> return None - | Some u -> - let player = mapBack u - let! events = getPlayerEvents connStr player - return Some { player with Events = events |> List.toArray } - with e -> - printfn $"Got an error{e.Message}" - return None - } +let updatePlayerStats (player : PlayerData) = + connStr + |> Sql.connect + |> Sql.parameters + [ ( "did" , Sql.string (string player.DiscordId) ) + ( "strength", Sql.int player.Stats.Strength.Amount ) + ( "focus", Sql.int player.Stats.Focus.Amount ) + ( "charisma", Sql.int player.Stats.Charisma.Amount ) + ( "luck", Sql.int player.Stats.Luck.Amount ) ] + |> Sql.query """ + WITH usr AS (SELECT id FROM "user" WHERE discord_id = @did) + UPDATE player_stat SET strength = @strength, focus = @focus, charisma = @charisma, luck = @luck, + updated_at = now() at time zone 'utc' + FROM usr WHERE usr.id = user_id; + """ + |> Sql.executeNonQueryAsync + |> Async.AwaitTask -let updatePlayer connStr (player : PlayerData) = +let tryFindPlayer (discordId : uint64) = async { + try + let! user = +// use cert = new X509Certificate2("~/Downloads/ca-certificate.crt") +// (Uri connStr) +// |> Sql.fromUriToConfig +// |> Sql.requireSslMode +// |> Sql.formatConnectionString +// |> Sql.clientCertificate cert + connStr + |> Sql.connect + |> Sql.parameters [ "did", Sql.string (string discordId) ] + |> Sql.query """ + SELECT discord_id, display_name, gbt, inventory, strength, focus, charisma, luck FROM "user" + WHERE discord_id = @did + """ + |> Sql.executeAsync (fun read -> { + DiscordId = read.string "discord_id" |> uint64 + Name = read.string "display_name" + Bank = read.int "gbt" * 1 + Inventory = read.intArray "inventory" |> Array.toList + Strength = read.int "strength" + Focus = read.int "focus" + Charisma = read.int "charm" + Luck = read.int "luck" + }) + |> Async.AwaitTask + match List.tryHead user with + | None -> return None + | Some u -> + let! events = getPlayerEvents u.DiscordId + let inventory = u.Inventory |> List.choose (fun id -> Armory.weapons |> List.tryFind (fun item -> item.Id = id)) + let strength = PlayerStats.calculateActiveStat StatId.Strength u.Strength inventory + let focus = PlayerStats.calculateActiveStat StatId.Focus u.Focus inventory + let charisma = PlayerStats.calculateActiveStat StatId.Charisma u.Charisma inventory + let luck = PlayerStats.calculateActiveStat StatId.Luck u.Luck inventory + return Some + { DiscordId = u.DiscordId + Name = u.Name + Inventory = inventory + Events = events + Stats = { Strength = strength ; Focus = focus ; Charisma = charisma ; Luck = luck } + Bank = u.Bank } + with e -> + printfn $"Got an error{e.Message}" + return None +} + +let updatePlayer (player : PlayerData) = connStr |> Sql.connect |> Sql.parameters [ "did", Sql.string (string player.DiscordId) "gbt", Sql.int (int player.Bank) - "str", Sql.int (player.Stats.Strength.Amount |> int) "inv", Sql.intArray (player.Inventory |> Array.ofList |> Array.map (fun item -> item.Id)) - ] - |> Sql.query """ - UPDATE "user" SET gbt = @gbt, strength = @str, inventory = @inv + "strength", Sql.int player.Stats.Strength.Amount + "focus", Sql.int player.Stats.Focus.Amount + "charisma", Sql.int player.Stats.Charisma.Amount + "luck", Sql.int player.Stats.Luck.Amount + ] |> Sql.query """ + UPDATE "user" SET gbt = @gbt, inventory = @inv, + strength = @strength, focus = @focus, charisma = @charisma, luck = @luck WHERE discord_id = @did """ |> Sql.executeNonQueryAsync |> Async.AwaitTask -let addAchievement connStr (did : uint64) (achievement : string) = +let addAchievement (did : uint64) (achievement : string) = connStr |> Sql.connect |> Sql.parameters @@ -114,7 +143,7 @@ let addAchievement connStr (did : uint64) (achievement : string) = |> Sql.executeNonQueryAsync |> Async.AwaitTask -let checkHasAchievement connStr (did : uint64) (achievement : string) = async { +let checkHasAchievement (did : uint64) (achievement : string) = async { let! result = connStr |> Sql.connect @@ -131,7 +160,7 @@ let checkHasAchievement connStr (did : uint64) (achievement : string) = async { return List.isEmpty result |> not } -let removeShieldEvent connStr (did : uint64) shieldId = +let removeShieldEvent (did : uint64) shieldId = connStr |> Sql.connect |> Sql.parameters @@ -144,7 +173,7 @@ let removeShieldEvent connStr (did : uint64) shieldId = |> Sql.executeNonQueryAsync |> Async.AwaitTask -let addPlayerEvent connStr (did : uint64) (playerEvent : PlayerEvent) = +let addPlayerEvent (did : uint64) (playerEvent : PlayerEvent) = let sqlParams , query = match playerEvent.Type with | Hacking h -> diff --git a/Bot/GameHelpers.fs b/Bot/GameHelpers.fs index 064a630..466be95 100644 --- a/Bot/GameHelpers.fs +++ b/Bot/GameHelpers.fs @@ -91,15 +91,15 @@ module WeaponClass = module Player = let getHackEvents player = player.Events - |> Array.filter (fun act -> match act.Type with PlayerEventType.Hacking h -> h.IsInstigator | _ -> false) + |> List.filter (fun act -> match act.Type with PlayerEventType.Hacking h -> h.IsInstigator | _ -> false) let getShieldEvents player = player.Events - |> Array.filter (fun act -> match act.Type with PlayerEventType.Shielding _ -> true | _ -> false) + |> List.filter (fun act -> match act.Type with PlayerEventType.Shielding _ -> true | _ -> false) let removeExpiredActions player = let actions = player.Events - |> Array.filter (fun (act : PlayerEvent) -> + |> List.filter (fun (act : PlayerEvent) -> let cooldown = System.TimeSpan.FromMinutes(int act.Cooldown) System.DateTime.UtcNow - act.Timestamp < cooldown) { player with Events = actions } @@ -108,24 +108,23 @@ module Player = module PlayerStats = // 4.17f would go from 100 to 0 in roughly 24 hours - let Strength = { Id = StatId.Strength ; BaseDecayRate = 4.17 ; BaseMinMax = Range(0, 100) } - let Focus = { Id = StatId.Focus ; BaseDecayRate = 4.17 ; BaseMinMax = Range(0, 100) } - let Luck = { Id = StatId.Luck ; BaseDecayRate = 4.17 ; BaseMinMax = Range(0, 100) } - let Charisma = { Id = StatId.Charisma ; BaseDecayRate = 4.17 ; BaseMinMax = Range(0, 100) } + let Strength = { Id = StatId.Strength ; BaseDecayRate = 4.17 ; BaseRange = Range.normalized } + let Focus = { Id = StatId.Focus ; BaseDecayRate = 4.17 ; BaseRange = Range.normalized } + let Luck = { Id = StatId.Luck ; BaseDecayRate = 4.17 ; BaseRange = Range.normalized } + let Charisma = { Id = StatId.Charisma ; BaseDecayRate = 4.17 ; BaseRange = Range.normalized } let stats = [ Strength ; Focus ; Luck ; Charisma ] - let calculateStatDecay (stat : PlayerStat) = - let statConfig = stats |> List.find (fun s -> s.Id = stat.Id) - let hoursElapsed = (DateTime.UtcNow - stat.LastRead).Hours - let totalDecay = float hoursElapsed * statConfig.BaseDecayRate - { stat with Amount = max stat.ModMinMax.Start.Value (stat.Amount - int totalDecay) ; LastRead = DateTime.UtcNow } - - let statConsumableMap = - [ ( StatId.Strength , 12 ) - ( StatId.Focus , 13 ) - ( StatId.Luck , 14 ) - ( StatId.Charisma , 15 ) ] + let calculateActiveStat statId amount items = + let statConfig = stats |> List.find (fun s -> s.Id = statId) +// let hoursElapsed = (DateTime.UtcNow - lastRead).Hours +// let totalDecay = float hoursElapsed * statConfig.BaseDecayRate + let modMinMax = + let min = items |> List.sumBy (fun item -> match item.Type with | Accessory(_,floorBoost,_) -> floorBoost | _ -> 0) + let max = items |> List.sumBy (fun item -> match item.Type with | Accessory(_,_,ceilBoost) -> ceilBoost | _ -> 0) + Range.create (statConfig.BaseRange.Min + min) (statConfig.BaseRange.Max + max) + let amountAfterDecay = modMinMax |> Range.constrain amount + { Id = statId ; Amount = amountAfterDecay ; ModRange = modMinMax ; LastRead = DateTime.UtcNow } module Arsenal = let battleItemFormat (items : Item list) = @@ -133,12 +132,12 @@ module Arsenal = | [] -> "None" | _ -> items |> List.map (fun item -> item.Name) |> String.concat ", " - let actionFormat (actions : PlayerEvent array) = + let actionFormat (actions : PlayerEvent List) = match actions with - | [||] -> "None" + | [] -> "None" | acts -> acts - |> Array.map (fun act -> + |> List.map (fun act -> match act.Type with | Hacking h -> let item = Armory.weapons |> Inventory.findHackById h.HackId @@ -149,12 +148,12 @@ module Arsenal = let cooldown = Messaging.getTimeText true (System.TimeSpan.FromMinutes(int act.Cooldown)) act.Timestamp $"{item.Name} Shield active for {cooldown}" | _ -> "") - |> Array.filter (System.String.IsNullOrWhiteSpace >> not) + |> List.filter (System.String.IsNullOrWhiteSpace >> not) |> String.concat "\n" let statusFormat p = let hacks = Player.getHackEvents p $"**Hacks:** {Inventory.filterByHacks p.Inventory |> battleItemFormat}\n **Shields:** {Inventory.filterByShields p.Inventory |> battleItemFormat}\n - **Hack Attacks:**\n{ hacks |> Array.take (min hacks.Length 10) |> actionFormat}\n + **Hack Attacks:**\n{ hacks |> List.take (min hacks.Length 10) |> actionFormat}\n **Active Shields:**\n{Player.getShieldEvents p |> actionFormat}" diff --git a/Bot/GameTypes.fs b/Bot/GameTypes.fs index 4aa8cff..c3cb71a 100644 --- a/Bot/GameTypes.fs +++ b/Bot/GameTypes.fs @@ -10,6 +10,13 @@ type mins [] type GBT +type Range = { Min : int ; Max : int } + +module Range = + let normalized = { Min = 0 ; Max = 100 } + let create min max = { Min = min ; Max = max } + let constrain value range = if value < range.Min then range.Min elif value > range.Max then range.Max else value + type ItemId = | Virus = 0 | RemoteAccess = 1 @@ -27,16 +34,16 @@ type StatId = type StatConfig = { Id : StatId BaseDecayRate : float - BaseMinMax : Range + BaseRange : Range } type PlayerStat = { Id : StatId Amount : int - ModMinMax : Range + ModRange : Range LastRead : DateTime } -with static member empty = { Id = StatId.Strength ; Amount = 0 ; ModMinMax = Range(0, 100) ; LastRead = DateTime.UtcNow} +with static member empty = { Id = StatId.Strength ; Amount = 0 ; ModRange = Range.normalized ; LastRead = DateTime.UtcNow} type Stats = { Strength : PlayerStat @@ -101,14 +108,15 @@ type AccessoryItem = { Name : string Price : int TargetStat : StatId - PassiveBoost : int + FloorBoost : int + CeilBoost : int } type ItemType = | Hack of power : int * hackClass : int * cooldown : int | Shield of shieldClass : int * cooldown : int | Food of targetStat : StatId * boostAmount : int - | Accessory of targetStat : StatId * passiveBoost : int + | Accessory of targetStat : StatId * floorBoost : int * ceilBoost : int and Item = { Id : int Name : string @@ -120,18 +128,18 @@ and PlayerData = { DiscordId : uint64 Name : string Inventory : Inventory - Events : PlayerEvent array + Events : PlayerEvent list Stats : Stats Bank : int } // Achievements : string array // XP : int -with member this.basicPlayer = { Id = this.DiscordId ; Name = this.Name } +with member this.toDiscordPlayer = { Id = this.DiscordId ; Name = this.Name } static member empty = { DiscordId = 0uL Name = "None" Inventory = [] - Events = [||] + Events = [] Stats = Stats.empty // Achievements = [||] // XP = 0 diff --git a/Bot/Games/HackerBattle.fs b/Bot/Games/HackerBattle.fs index 31ccd1e..2acbad3 100644 --- a/Bot/Games/HackerBattle.fs +++ b/Bot/Games/HackerBattle.fs @@ -85,7 +85,7 @@ let updateCombatants successfulHack (attacker : PlayerData) (defender : PlayerDa let event isDefenderEvent = let hackEvent = { HackId = hack.Id - Adversary = if isDefenderEvent then attacker.basicPlayer else defender.basicPlayer + Adversary = if isDefenderEvent then attacker.toDiscordPlayer else defender.toDiscordPlayer IsInstigator = not isDefenderEvent Success = successfulHack } @@ -93,10 +93,10 @@ let updateCombatants successfulHack (attacker : PlayerData) (defender : PlayerDa Timestamp = DateTime.UtcNow Cooldown = if isDefenderEvent then int WeaponClass.SameTargetAttackCooldown.TotalMinutes * 1 else hack.Cooldown } - [ DbService.updatePlayer GuildEnvironment.pgDb <| updatePlayer prize (event false) attacker - DbService.updatePlayer GuildEnvironment.pgDb <| updatePlayer -prize (event true) defender - DbService.addPlayerEvent GuildEnvironment.pgDb attacker.DiscordId (event false) - DbService.addPlayerEvent GuildEnvironment.pgDb defender.DiscordId (event true) ] + [ DbService.updatePlayer <| updatePlayer prize (event false) attacker + DbService.updatePlayer <| updatePlayer -prize (event true) defender + DbService.addPlayerEvent attacker.DiscordId (event false) + DbService.addPlayerEvent defender.DiscordId (event true) ] |> Async.Parallel |> Async.Ignore @@ -153,7 +153,7 @@ let handleAttack (ctx : IDiscordContext) = let hack = Armory.weapons |> Inventory.findHackById hackId let hackAsItem = Inventory.hackToItem hack let resultId , targetId = UInt64.TryParse tokens.[2] - let! resultTarget = DbService.tryFindPlayer GuildEnvironment.pgDb targetId + let! resultTarget = DbService.tryFindPlayer targetId match resultTarget , true , resultId with | Some defender , true , true -> @@ -202,9 +202,9 @@ let handleDefense (ctx : IDiscordContext) = Cooldown = shield.Cooldown Timestamp = DateTime.UtcNow } - do! DbService.updatePlayer GuildEnvironment.pgDb p + do! DbService.updatePlayer p |> Async.Ignore - do! DbService.addPlayerEvent GuildEnvironment.pgDb p.DiscordId defense + do! DbService.addPlayerEvent p.DiscordId defense |> Async.Ignore let builder = DiscordMessageBuilder() builder.WithContent($"{ctx.GetDiscordMember().Username} has protected their system!") |> ignore @@ -224,7 +224,7 @@ let arsenal (ctx : IDiscordContext) = builder.AddEmbed(embed) |> ignore builder.IsEphemeral <- true do! ctx.FollowUp(builder) |> Async.AwaitTask - do! DbService.updatePlayer GuildEnvironment.pgDb updatedPlayer + do! DbService.updatePlayer updatedPlayer |> Async.Ignore }) diff --git a/Bot/Games/SlotMachine.fs b/Bot/Games/SlotMachine.fs index 62f54a3..d40a1b3 100644 --- a/Bot/Games/SlotMachine.fs +++ b/Bot/Games/SlotMachine.fs @@ -23,10 +23,10 @@ type SlotMachine() = || (results.[0] <> results.[1] && results.[1] <> results.[2] && results.[0] <> results.[2]) if winConditions then - do! DbService.updatePlayer GuildEnvironment.pgDb { player with Bank = player.Bank + 10 } + do! DbService.updatePlayer { player with Bank = player.Bank + 10 } |> Async.Ignore else - do! DbService.updatePlayer GuildEnvironment.pgDb { player with Bank = max (player.Bank - 1) 0 } + do! DbService.updatePlayer { player with Bank = max (player.Bank - 1) 0 } |> Async.Ignore diff --git a/Bot/Games/Store.fs b/Bot/Games/Store.fs index 6b9029b..37b37a8 100644 --- a/Bot/Games/Store.fs +++ b/Bot/Games/Store.fs @@ -107,7 +107,7 @@ let handleBuyItem (ctx : IDiscordContext) itemId = |> handleResultWithResponse ctx (fun player -> async { let newBalance = player.Bank - item.Price let p = { player with Bank = newBalance ; Inventory = item::player.Inventory } - do! DbService.updatePlayer GuildEnvironment.pgDb p |> Async.Ignore + do! DbService.updatePlayer p |> Async.Ignore do! sendFollowUpMessage ctx $"Successfully purchased {item.Name}! You now have {newBalance} 💰$GBT remaining" }) }) @@ -125,8 +125,8 @@ let handleSell (ctx : IDiscordContext) itemId = Inventory = player.Inventory |> List.filter (fun i -> i.Id <> itemId) } do! - [ DbService.updatePlayer GuildEnvironment.pgDb updatedPlayer |> Async.Ignore - DbService.removeShieldEvent GuildEnvironment.pgDb updatedPlayer.DiscordId itemId |> Async.Ignore + [ DbService.updatePlayer updatedPlayer |> Async.Ignore + DbService.removeShieldEvent updatedPlayer.DiscordId itemId |> Async.Ignore sendFollowUpMessage ctx $"Sold {item.Type} {item.Name} for {item.Price}! Current Balance: {updatedPlayer.Bank}" ] |> Async.Parallel |> Async.Ignore diff --git a/Bot/Games/Thief.fs b/Bot/Games/Thief.fs index 5a7264a..47990e5 100644 --- a/Bot/Games/Thief.fs +++ b/Bot/Games/Thief.fs @@ -175,15 +175,15 @@ let handleSteal (ctx : IDiscordContext) = | true -> let embed = getResultEmbed' Success do! Messaging.sendFollowUpEmbed ctx (embed.Build()) - match! DbService.tryFindPlayer GuildEnvironment.pgDb targetId with + match! DbService.tryFindPlayer targetId with | Some t -> let mugged = { - Type = Stealing ( false , thief.basicPlayer ) + Type = Stealing ( false , thief.toDiscordPlayer ) Timestamp = DateTime.UtcNow Cooldown = VictimRecovery.Minutes * 1 } - do! DbService.updatePlayer GuildEnvironment.pgDb { t with Bank = max (t.Bank - prize) 0 } |> Async.Ignore - do! DbService.addPlayerEvent GuildEnvironment.pgDb victim.DiscordId mugged |> Async.Ignore + do! DbService.updatePlayer { t with Bank = max (t.Bank - prize) 0 } |> Async.Ignore + do! DbService.addPlayerEvent victim.DiscordId mugged |> Async.Ignore | None -> () let stole = { @@ -191,8 +191,8 @@ let handleSteal (ctx : IDiscordContext) = Cooldown = ThiefCooldown.Minutes * 1 Timestamp = DateTime.UtcNow } - do! DbService.updatePlayer GuildEnvironment.pgDb { thief with Bank = thief.Bank + prize } |> Async.Ignore - do! DbService.addPlayerEvent GuildEnvironment.pgDb victim.DiscordId stole |> Async.Ignore + do! DbService.updatePlayer { thief with Bank = thief.Bank + prize } |> Async.Ignore + do! DbService.addPlayerEvent victim.DiscordId stole |> Async.Ignore let builder = DiscordMessageBuilder() builder.WithContent($"{thief.Name} stole {prize} from <@{victim.DiscordId}>!") |> ignore let channel = ctx.GetGuild().GetChannel(GuildEnvironment.channelEventsHackerBattle) @@ -206,7 +206,7 @@ let handleSteal (ctx : IDiscordContext) = Cooldown = ThiefCooldown.Minutes * 1 Timestamp = DateTime.UtcNow } - do! DbService.addPlayerEvent GuildEnvironment.pgDb victim.DiscordId imprisoned |> Async.Ignore + do! DbService.addPlayerEvent victim.DiscordId imprisoned |> Async.Ignore do! Messaging.sendFollowUpEmbed ctx (embed.Build()) do! Async.Sleep 2000 let role = ctx.GetGuild().GetRole(GuildEnvironment.rolePrisoner) diff --git a/Bot/Games/Trainer.fs b/Bot/Games/Trainer.fs index 24df899..5a321fc 100644 --- a/Bot/Games/Trainer.fs +++ b/Bot/Games/Trainer.fs @@ -146,9 +146,9 @@ let handleHack (ctx : IDiscordContext) = let sb = StringBuilder("Here, ") - let! completed = DbService.checkHasAchievement GuildEnvironment.pgDb player.DiscordId trainerAchievement + let! completed = DbService.checkHasAchievement player.DiscordId trainerAchievement if not completed then - do! DbService.addAchievement GuildEnvironment.pgDb player.DiscordId trainerAchievement + do! DbService.addAchievement player.DiscordId trainerAchievement |> Async.Ignore sb.Append($"I'm going to gift you a hack,`{defaultHack.Name}` and a shield, `{defaultShield.Name}`") |> ignore @@ -180,15 +180,15 @@ let handleArsenal (ctx : IDiscordContext) = Player.removeExpiredActions player if not hasStockWeapons then do! - [ DbService.addPlayerEvent GuildEnvironment.pgDb player.DiscordId TrainerEvents.[0] - DbService.addPlayerEvent GuildEnvironment.pgDb player.DiscordId TrainerEvents.[1] - DbService.updatePlayer GuildEnvironment.pgDb updatedPlayer ] + [ DbService.addPlayerEvent player.DiscordId TrainerEvents.[0] + DbService.addPlayerEvent player.DiscordId TrainerEvents.[1] + DbService.updatePlayer updatedPlayer ] |> Async.Parallel |> Async.Ignore let embed = Embeds.getArsenalEmbed updatedPlayer do! ctx.FollowUp(embed) |> Async.AwaitTask - let! completed = DbService.checkHasAchievement GuildEnvironment.pgDb player.DiscordId trainerAchievement + let! completed = DbService.checkHasAchievement player.DiscordId trainerAchievement if not completed then do! Async.Sleep 3000 let rewards = [ $"{defaultHack.Name} Hack" ; $"{defaultShield.Name} Shield" ] diff --git a/Bot/GuildEnvironment.fs b/Bot/GuildEnvironment.fs index df3c973..c968e81 100644 --- a/Bot/GuildEnvironment.fs +++ b/Bot/GuildEnvironment.fs @@ -11,7 +11,7 @@ DotEnv.Load(DotEnvOptions(envFilePaths = [ "../../../../.dev.env" ], overwriteEx let getVar str = Environment.GetEnvironmentVariable(str) let getId str = getVar str |> uint64 -let pgDb = (getVar "DATABASE_URL").Replace("postgresql://", "postgres://").Replace("?sslmode=require", "") +let connectionString = (getVar "DATABASE_URL").Replace("postgresql://", "postgres://").Replace("?sslmode=require", "") let guildId = getId "DISCORD_GUILD" let tokenPlayerInteractions = getVar "TOKEN_PLAYER_INTERACTIONS" diff --git a/Bot/PlayerInteractions.fs b/Bot/PlayerInteractions.fs index eaa0623..170c80d 100644 --- a/Bot/PlayerInteractions.fs +++ b/Bot/PlayerInteractions.fs @@ -10,7 +10,7 @@ let executePlayerAction (ctx : IDiscordContext) (dispatch : PlayerData -> Async< async { let builder = DiscordInteractionResponseBuilder().AsEphemeral(true) do! ctx.Respond(InteractionResponseType.DeferredChannelMessageWithSource, builder) |> Async.AwaitTask - let! playerResult = tryFindPlayer GuildEnvironment.pgDb (ctx.GetDiscordMember().Id) + let! playerResult = tryFindPlayer (ctx.GetDiscordMember().Id) match playerResult with | Some player -> do! dispatch player | None -> do! Messaging.sendFollowUpMessage ctx "You are currently not a hacker, first use the /redpill command to become one" @@ -23,8 +23,8 @@ let executePlayerActionWithTarget (targetPlayer : DiscordUser) (ctx : IDiscordCo builder.Content <- "Content" do! ctx.Respond(InteractionResponseType.DeferredChannelMessageWithSource, builder) |> Async.AwaitTask let! players = - [ tryFindPlayer GuildEnvironment.pgDb (ctx.GetDiscordMember().Id) - tryFindPlayer GuildEnvironment.pgDb targetPlayer.Id ] + [ tryFindPlayer (ctx.GetDiscordMember().Id) + tryFindPlayer targetPlayer.Id ] |> Async.Parallel match players.[0] , players.[1] with | Some player , Some target -> do! dispatch player target @@ -43,8 +43,8 @@ let executePlayerActionWithTargetId defer (targetId : uint64) (ctx : IDiscordCon if defer then do! ctx.Respond(InteractionResponseType.DeferredChannelMessageWithSource, builder) |> Async.AwaitTask let! players = - [ tryFindPlayer GuildEnvironment.pgDb (ctx.GetDiscordMember().Id) - tryFindPlayer GuildEnvironment.pgDb targetId ] + [ tryFindPlayer (ctx.GetDiscordMember().Id) + tryFindPlayer targetId ] |> Async.Parallel match players.[0] , players.[1] with | Some player , Some target -> do! dispatch player target From 8a57d3305ebdaea87e69bc9561217046abd97187 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Tue, 1 Mar 2022 21:48:44 +0700 Subject: [PATCH 10/28] Store for food and accessory items, read stats --- Bot/DbService.fs | 8 +-- Bot/Embeds.fs | 4 +- Bot/GameHelpers.fs | 40 +++++++++++---- Bot/GameTypes.fs | 18 +++++-- Bot/Games/HackerBattle.fs | 16 +++--- Bot/Games/Store.fs | 73 +++++++++++++++++++++++++-- Bot/Games/Thief.fs | 4 +- Bot/Games/Trainer.fs | 6 +-- Bot/GuildEnvironment.fs | 4 ++ Bot/Items.json | 102 +++++++++++++++++++++++++++++++++++++- 10 files changed, 237 insertions(+), 38 deletions(-) diff --git a/Bot/DbService.fs b/Bot/DbService.fs index 0402e58..31de54e 100644 --- a/Bot/DbService.fs +++ b/Bot/DbService.fs @@ -78,14 +78,16 @@ let tryFindPlayer (discordId : uint64) = async { SELECT discord_id, display_name, gbt, inventory, strength, focus, charisma, luck FROM "user" WHERE discord_id = @did """ - |> Sql.executeAsync (fun read -> { + |> Sql.executeAsync (fun read -> + let inv = read.intArray "inventory" + { DiscordId = read.string "discord_id" |> uint64 Name = read.string "display_name" Bank = read.int "gbt" * 1 - Inventory = read.intArray "inventory" |> Array.toList + Inventory = inv |> Array.toList Strength = read.int "strength" Focus = read.int "focus" - Charisma = read.int "charm" + Charisma = read.int "charisma" Luck = read.int "luck" }) |> Async.AwaitTask diff --git a/Bot/Embeds.fs b/Bot/Embeds.fs index c2a88f2..a27dbfd 100644 --- a/Bot/Embeds.fs +++ b/Bot/Embeds.fs @@ -33,7 +33,7 @@ let constructButtons (actionId: string) (buttonInfo : string) (player: PlayerDat |> List.map (fun item -> let action = player.Events - |> Array.tryFind (fun i -> + |> List.tryFind (fun i -> match i.Type with | Hacking h -> h.HackId = item.Id && h.IsInstigator | Shielding id -> id = item.Id @@ -101,7 +101,7 @@ let responseSuccessfulHack earnedMoney (targetId : uint64) amountTaken (hack : H let responseCreatedShield (shield : ShieldItem) = let embed = DiscordEmbedBuilder().WithImageUrl(getItemGif shield.Id) embed.Title <- "Mounted Shield" - embed.Description <- $"Mounted {shield.Name} shield for {TimeSpan.FromMinutes(int shield.Cooldown).Hours} hours" + embed.Description <- $"Mounted {shield.Name} shield for {TimeSpan.FromMinutes(int shield.Cooldown).TotalHours} hours" DiscordFollowupMessageBuilder() .AddEmbed(embed) diff --git a/Bot/GameHelpers.fs b/Bot/GameHelpers.fs index 466be95..a80bdea 100644 --- a/Bot/GameHelpers.fs +++ b/Bot/GameHelpers.fs @@ -21,7 +21,6 @@ module Inventory = Class = hackClass Cooldown = cooldown } - let itemToShield item hackClass cooldown = { Id = item.Id Name = item.Name @@ -29,31 +28,48 @@ module Inventory = Class = hackClass Cooldown = cooldown } + let itemToFood item targetStat boostAmount = { + Id = item.Id + Name = item.Name + Price = item.Price + TargetStat = targetStat + BoostAmount = boostAmount + } let hackToItem (hack : HackItem) = { Id = hack.Id Name = hack.Name Price = hack.Price - Type = Hack (hack.Power, hack.Class, hack.Cooldown) + Details = Hack (hack.Power, hack.Class, hack.Cooldown) } let shieldToItem (shield : ShieldItem) = { Id = shield.Id Name = shield.Name Price = shield.Price - Type = Shield (shield.Class, shield.Cooldown) + Details = Shield (shield.Class, shield.Cooldown) } let filterByHacks inventory = - inventory |> List.filter (fun item -> match item.Type with Hack _ -> true | _ -> false) + inventory |> List.filter (fun item -> match item.Details with Hack _ -> true | _ -> false) let filterByShields inventory = - inventory |> List.filter (fun item -> match item.Type with Shield _ -> true | _ -> false) + inventory |> List.filter (fun item -> match item.Details with Shield _ -> true | _ -> false) + let filterByWeapons inventory = + inventory |> List.filter (fun item -> match item.Details with Hack _ | Shield _ -> true | _ -> false) + let filterByFood inventory = + inventory |> List.filter (fun item -> match item.Details with Food _ -> true | _ -> false) + let filterByAccessories inventory = + inventory |> List.filter (fun item -> match item.Details with Accessory _ -> true | _ -> false) let getHackItems inventory = inventory - |> List.choose (fun item -> match item.Type with Hack (p,cl,co) -> Some (itemToHack item p cl co) | _ -> None) + |> List.choose (fun item -> match item.Details with Hack (p,cl,co) -> Some (itemToHack item p cl co) | _ -> None) |> List.sortBy (fun item -> item.Id) let getShieldItems inventory = inventory - |> List.choose (fun item -> match item.Type with Shield (cl,co) -> Some (itemToShield item cl co) | _ -> None) + |> List.choose (fun item -> match item.Details with Shield (cl,co) -> Some (itemToShield item cl co) | _ -> None) + |> List.sortBy (fun item -> item.Id) + let getFoodItems inventory = + inventory + |> List.choose (fun item -> match item.Details with Food(t,b) -> Some (itemToFood item t b) | _ -> None) |> List.sortBy (fun item -> item.Id) let findItemById id inventory = inventory |> List.find (fun item -> item.Id = id) let tryFindItemById id inventory = inventory |> List.tryFind (fun item -> item.Id = id) @@ -61,6 +77,8 @@ module Inventory = inventory |> getHackItems |> List.pick (fun item -> if item.Id = id then Some item else None) let findShieldById id inventory = inventory |> getShieldItems |> List.pick (fun item -> if item.Id = id then Some item else None) + let findFoodById id inventory = + inventory |> getFoodItems |> List.pick (fun item -> if item.Id = id then Some item else None) let tryFindHackById id inventory = inventory |> getHackItems |> List.tryFind (fun item -> item.Id = id) let tryFindShieldById id inventory = @@ -70,14 +88,14 @@ module WeaponClass = let SameTargetAttackCooldown = System.TimeSpan.FromHours(1) let getClassButtonColor item = - match item.Type with + match item.Details with | Hack (_,0,_) | Shield (0,_) -> ButtonStyle.Danger | Hack (_,1,_) | Shield (1,_) -> ButtonStyle.Primary | Hack (_,2,_) | Shield (2,_) -> ButtonStyle.Success | _ -> ButtonStyle.Primary let getClassEmbedColor item = - match item.Type with + match item.Details with | Hack (_,0,_) | Shield (0,_) -> DiscordColor.Red | Hack (_,1,_) | Shield (1,_) -> DiscordColor.Blurple | Hack (_,2,_) | Shield (2,_) -> DiscordColor.Green @@ -120,8 +138,8 @@ module PlayerStats = // let hoursElapsed = (DateTime.UtcNow - lastRead).Hours // let totalDecay = float hoursElapsed * statConfig.BaseDecayRate let modMinMax = - let min = items |> List.sumBy (fun item -> match item.Type with | Accessory(_,floorBoost,_) -> floorBoost | _ -> 0) - let max = items |> List.sumBy (fun item -> match item.Type with | Accessory(_,_,ceilBoost) -> ceilBoost | _ -> 0) + let min = items |> List.sumBy (fun item -> match item.Details with | Accessory(_,floorBoost,_) -> floorBoost | _ -> 0) + let max = items |> List.sumBy (fun item -> match item.Details with | Accessory(_,_,ceilBoost) -> ceilBoost | _ -> 0) Range.create (statConfig.BaseRange.Min + min) (statConfig.BaseRange.Max + max) let amountAfterDecay = modMinMax |> Range.constrain amount { Id = statId ; Amount = amountAfterDecay ; ModRange = modMinMax ; LastRead = DateTime.UtcNow } diff --git a/Bot/GameTypes.fs b/Bot/GameTypes.fs index c3cb71a..8794e15 100644 --- a/Bot/GameTypes.fs +++ b/Bot/GameTypes.fs @@ -24,12 +24,16 @@ type ItemId = | Firewall = 6 | Encryption = 7 | Cypher = 8 + | ProteinPowder = 12 + | ToroLoco = 13 + | Cigs = 14 + | MoonPie = 15 type StatId = | Strength = 0 | Focus = 1 - | Luck = 2 - | Charisma = 3 + | Charisma = 2 + | Luck = 3 type StatConfig = { Id : StatId @@ -112,7 +116,14 @@ type AccessoryItem = { CeilBoost : int } +[] type ItemType = + | Hack + | Shield + | Food + | Accessory + +type ItemDetails = | Hack of power : int * hackClass : int * cooldown : int | Shield of shieldClass : int * cooldown : int | Food of targetStat : StatId * boostAmount : int @@ -121,7 +132,8 @@ and Item = { Id : int Name : string Price : int - Type : ItemType +// Type : ItemType + Details : ItemDetails } and Inventory = Item list and PlayerData = { diff --git a/Bot/Games/HackerBattle.fs b/Bot/Games/HackerBattle.fs index 2acbad3..62e983e 100644 --- a/Bot/Games/HackerBattle.fs +++ b/Bot/Games/HackerBattle.fs @@ -19,7 +19,7 @@ let checkAlreadyHackedTarget defender attacker = defender |> Player.removeExpiredActions |> fun d -> d.Events - |> Array.tryFind (fun event -> + |> List.tryFind (fun event -> match event.Type with | Hacking h -> h.Adversary.Id = attacker.DiscordId && h.IsInstigator = false | _ -> false) @@ -32,7 +32,7 @@ let checkAlreadyHackedTarget defender attacker = let checkWeaponHasCooldown (weapon : Item) attacker = attacker.Events - |> Array.tryFind (fun a -> + |> List.tryFind (fun a -> match a.Type with | Hacking h -> h.HackId = weapon.Id && h.IsInstigator | Shielding id -> id = weapon.Id @@ -56,9 +56,9 @@ let checkPlayerOwnsWeapon (item : Item) player = let checkPlayerHasShieldSlotsAvailable player = let updatedPlayer = player |> Player.removeExpiredActions let defenses = Player.getShieldEvents updatedPlayer - match defenses |> Array.length >= 3 with + match defenses |> List.length >= 3 with | true -> - let event = defenses |> Array.rev |> Array.head // This should be the next expiring timestamp + let event = defenses |> List.rev |> List.head // This should be the next expiring timestamp let cooldown = getTimeText true (TimeSpan.FromMinutes(int event.Cooldown)) event.Timestamp Error $"You are only allowed three shields at a time. Wait {cooldown} to add another shield" | false -> Ok updatedPlayer @@ -72,16 +72,16 @@ let runHackerBattle defender (hack : HackItem) = defender |> Player.removeExpiredActions |> fun p -> p.Events - |> Array.choose (fun event -> + |> List.choose (fun event -> match event.Type with | Shielding id -> defender.Inventory |> Inventory.getShieldItems |> List.find (fun item -> item.Id = id) |> Some | _ -> None) - |> Array.map (fun shield -> if hack.Class = shield.Class then Weak else Strong) - |> Array.contains Weak + |> List.map (fun shield -> if hack.Class = shield.Class then Weak else Strong) + |> List.contains Weak let updateCombatants successfulHack (attacker : PlayerData) (defender : PlayerData) (hack : HackItem) prize = let updatePlayer amount attack p = - { p with Events = Array.append [| attack |] p.Events ; Bank = max (p.Bank + amount) 0 } + { p with Events = attack::p.Events ; Bank = max (p.Bank + amount) 0 } let event isDefenderEvent = let hackEvent = { HackId = hack.Id diff --git a/Bot/Games/Store.fs b/Bot/Games/Store.fs index 37b37a8..a80b070 100644 --- a/Bot/Games/Store.fs +++ b/Bot/Games/Store.fs @@ -15,7 +15,7 @@ let getBuyItemsEmbed (playerInventory : Inventory) (storeInventory : Inventory) storeInventory |> List.map (fun item -> let embed = DiscordEmbedBuilder() - match item.Type with + match item.Details with | Hack(power,_,cooldown) -> embed.AddField($"$GBT Reward |", string power, true) .AddField("Cooldown |", $"{TimeSpan.FromMinutes(int cooldown).Minutes} minutes", true) @@ -27,7 +27,15 @@ let getBuyItemsEmbed (playerInventory : Inventory) (storeInventory : Inventory) .AddField("Active For |", $"{TimeSpan.FromMinutes(int cooldown).Hours} hours", true) .WithThumbnail(Embeds.getItemIcon item.Id) |> ignore - | _ -> () + | Food(targetStat, boostAmount) -> + embed.AddField($"Stat |", $"{targetStat}", true) + .AddField($"Amount |", $"+{boostAmount}", true) |> ignore + | Accessory(targetStat, floorBoost, ceilBoost) -> + embed.AddField($"Stat |", $"{targetStat}", true) |> ignore + if floorBoost > 0 then + embed.AddField($"Min Boost |", $"+{floorBoost}", true) |> ignore + if ceilBoost > 0 then + embed.AddField($"Max Boost |", $"+{ceilBoost}", true) |> ignore embed .AddField("Price 💰", (if item.Price = 0 then "Free" else $"{item.Price} $GBT"), true) .WithColor(WeaponClass.getClassEmbedColor item) @@ -62,6 +70,26 @@ let getSellEmbed (items : Item list) = .AddComponents(buttons) .AsEphemeral(true) +let getConsumeEmbed (items : Item list) = + let embeds , buttons = + items + |> List.groupBy (fun item -> item.Id) + |> List.map (fun (itemId , items ) -> + let item = List.head items + let foodItem = Inventory.findFoodById itemId items + DiscordEmbedBuilder() + .AddField($"{foodItem.Name}", $"Total {items.Length}\nBoosts {foodItem.TargetStat} +{foodItem.BoostAmount}", true) + .WithTitle($"Food Items") + .WithColor(WeaponClass.getClassEmbedColor item) + .Build() + , DiscordButtonComponent(WeaponClass.getClassButtonColor item, $"Sell-{id}", $"Sell {item.Name}") :> DiscordComponent) + |> List.unzip + + DiscordFollowupMessageBuilder() + .AddEmbeds(embeds) + .AddComponents(buttons) + .AsEphemeral(true) + let checkHasSufficientFunds (item : Item) player = if player.Bank - item.Price >= 0 then Ok player @@ -127,12 +155,24 @@ let handleSell (ctx : IDiscordContext) itemId = do! [ DbService.updatePlayer updatedPlayer |> Async.Ignore DbService.removeShieldEvent updatedPlayer.DiscordId itemId |> Async.Ignore - sendFollowUpMessage ctx $"Sold {item.Type} {item.Name} for {item.Price}! Current Balance: {updatedPlayer.Bank}" ] + sendFollowUpMessage ctx $"Sold {item.Details} {item.Name} for {item.Price}! Current Balance: {updatedPlayer.Bank}" ] |> Async.Parallel |> Async.Ignore }) }) +//let inventory (ctx : IDiscordContext) = +// executePlayerAction ctx (fun player -> async { +// player.Inventory +// |> List.groupBy (fun item -> item.Details) +// }) +// +// +//let consume (ctx : IDiscordContext) = +// executePlayerAction ctx (fun player -> async { +// +// }) + let handleStoreEvents (_ : DiscordClient) (event : ComponentInteractionCreateEventArgs) = let ctx = DiscordEventContext event :> IDiscordContext let id = ctx.GetInteractionId() @@ -160,15 +200,38 @@ type Store() = do! Messaging.sendSimpleResponse ctx msg } - [] - member _.BuyHack (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (buy Inventory.filterByHacks) + let checkChannel (ctx : IDiscordContext) = + match ctx.GetChannel().Id with + | id when id = GuildEnvironment.channelBackAlley -> buy Inventory.filterByHacks ctx + | id when id = GuildEnvironment.channelArmory -> buy Inventory.filterByShields ctx + | id when id = GuildEnvironment.channelMarket -> buy Inventory.filterByFood ctx + | id when id = GuildEnvironment.channelAccessoryShop -> buy Inventory.filterByAccessories ctx + | _ -> + task { + let msg = $"This channel doesn't have any items to sell" + do! Messaging.sendSimpleResponse ctx msg + } + + [] + member _.BuyItem (ctx : InteractionContext) = checkChannel (DiscordInteractionContext(ctx)) [] member this.BuyShield (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (buy Inventory.filterByShields) + [] + member this.BuyFood (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (buy Inventory.filterByFood) + [] member this.SellHack (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (sell "Hacks" Inventory.filterByHacks) [] member this.SellShield (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (sell "Shields" Inventory.filterByShields) + [] + member this.Consume (ctx : InteractionContext) = + enforceChannel (DiscordInteractionContext(ctx)) (sell "Shields" Inventory.filterByShields) + + [] + member this.Inventory (ctx : InteractionContext) = + enforceChannel (DiscordInteractionContext(ctx)) (sell "Shields" Inventory.filterByShields) + diff --git a/Bot/Games/Thief.fs b/Bot/Games/Thief.fs index 47990e5..233cc7d 100644 --- a/Bot/Games/Thief.fs +++ b/Bot/Games/Thief.fs @@ -72,7 +72,7 @@ let checkVictimStealingCooldown defender attacker = defender |> Player.removeExpiredActions |> fun p -> p.Events - |> Array.tryFind (fun e -> + |> List.tryFind (fun e -> match e.Type with Stealing _ -> true | _ -> false) |> function | Some act -> @@ -95,7 +95,7 @@ let checkThiefCooldown attacker = attacker |> Player.removeExpiredActions |> fun p -> p.Events - |> Array.tryFind (fun pe -> match pe.Type with Stealing(instigator, _) -> instigator | _ -> false) + |> List.tryFind (fun pe -> match pe.Type with Stealing(instigator, _) -> instigator | _ -> false) |> function | Some act -> let cooldown = ThiefCooldown - (DateTime.UtcNow - act.Timestamp) diff --git a/Bot/Games/Trainer.fs b/Bot/Games/Trainer.fs index 5a321fc..fe5d53f 100644 --- a/Bot/Games/Trainer.fs +++ b/Bot/Games/Trainer.fs @@ -12,7 +12,7 @@ let Sensei = { Id = GuildEnvironment.botIdHackerBattle ; Name = "Sensei" } let defaultHack = Armory.weapons |> Inventory.findHackById (int ItemId.Virus) let defaultShield = Armory.weapons |> Inventory.findShieldById (int ItemId.Firewall) -let TrainerEvents = [| +let TrainerEvents = [ { Timestamp = System.DateTime.UtcNow Cooldown = defaultHack.Cooldown Type = Hacking { @@ -23,7 +23,7 @@ let TrainerEvents = [| { Timestamp = System.DateTime.UtcNow Cooldown = defaultShield.Cooldown Type = Shielding defaultShield.Id } -|] +] let sendInitialEmbed (client : DiscordClient) = async { @@ -173,7 +173,7 @@ let handleArsenal (ctx : IDiscordContext) = let updatedPlayer = if not hasStockWeapons then { Player.removeExpiredActions player with - Events = TrainerEvents |> Array.append player.Events + Events = TrainerEvents @ player.Events Inventory = Inventory.hackToItem defaultHack::Inventory.shieldToItem defaultShield::player.Inventory } else diff --git a/Bot/GuildEnvironment.fs b/Bot/GuildEnvironment.fs index c968e81..0221b09 100644 --- a/Bot/GuildEnvironment.fs +++ b/Bot/GuildEnvironment.fs @@ -21,7 +21,11 @@ let tokenStore = getVar "TOKEN_STORE" let channelEventsHackerBattle = getId "CHANNEL_EVENTS_HACKER_BATTLE" let channelTraining = getId "CHANNEL_TRAINING" let channelArmory = getId "CHANNEL_ARMORY" +let channelBackAlley = getId "CHANNEL_BACKALLEY" let channelBattle = getId "CHANNEL_BATTLE" +let channelMarket = getId "CHANNEL_MARKET" +let channelAccessoryShop = getId "CHANNEL_ACCESSORIES" + //let channelThievery = getId "CHANNEL_THIEVERY" let botIdHackerBattle = getId "BOT_HACKER_BATTLE" let botIdArmory = getId "BOT_ARMORY" diff --git a/Bot/Items.json b/Bot/Items.json index 5ba6228..c99fd23 100644 --- a/Bot/Items.json +++ b/Bot/Items.json @@ -14,7 +14,7 @@ }, { "Id": 1, - "Name": "RemoteAccess", + "Name": "Remote Access", "Price": 500, "Type": { "Case": "Hack", @@ -73,5 +73,105 @@ 380 ] } + }, + { + "Id": 12, + "Name": "Protein Powder", + "Price": 50, + "Type": { + "Case": "Food", + "Fields": [ + 0, + 30 + ] + } + }, + { + "Id": 13, + "Name": "Toro Loco", + "Price": 50, + "Type": { + "Case": "Food", + "Fields": [ + 1, + 30 + ] + } + }, + { + "Id": 14, + "Name": "Cigarettes", + "Price": 50, + "Type": { + "Case": "Food", + "Fields": [ + 2, + 30 + ] + } + }, + { + "Id": 15, + "Name": "Moon Pie", + "Price": 50, + "Type": { + "Case": "Food", + "Fields": [ + 3, + 30 + ] + } + }, + { + "Id": 20, + "Name": "Kettle Bell", + "Price": 250, + "Type": { + "Case": "Accessory", + "Fields": [ + 0, + 25, + 0 + ] + } + }, + { + "Id": 21, + "Name": "Headphones", + "Price": 250, + "Type": { + "Case": "Accessory", + "Fields": [ + 1, + 25, + 0 + ] + } + }, + { + "Id": 22, + "Name": "Silk Shirt", + "Price": 250, + "Type": { + "Case": "Accessory", + "Fields": [ + 2, + 0, + 25 + ] + } + }, + { + "Id": 23, + "Name": "Buddha Keychain", + "Price": 250, + "Type": { + "Case": "Accessory", + "Fields": [ + 3, + 0, + 25 + ] + } } ] From f01fc28a0c3fe996457afabf9ae6fcf5eaa28c83 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Wed, 2 Mar 2022 23:10:01 +0700 Subject: [PATCH 11/28] Another pass on item modeling --- Bot/Embeds.fs | 26 +-- Bot/GameHelpers.fs | 142 ++++++----------- Bot/GameTypes.fs | 110 +++++++------ Bot/Games/HackerBattle.fs | 53 +++++-- Bot/Games/Store.fs | 70 ++++----- Bot/Games/Trainer.fs | 24 +-- Bot/Items.json | 322 +++++++++++++++++++++----------------- 7 files changed, 394 insertions(+), 353 deletions(-) diff --git a/Bot/Embeds.fs b/Bot/Embeds.fs index a27dbfd..b4d8e28 100644 --- a/Bot/Embeds.fs +++ b/Bot/Embeds.fs @@ -28,13 +28,13 @@ let getItemGif id = | ItemId.Cypher -> "https://s10.gifyu.com/images/Cypher-Smaller.gif" | _ -> hackGif -let constructButtons (actionId: string) (buttonInfo : string) (player: PlayerData) items ignoreCooldown = +let constructButtons (actionId: string) (buttonInfo : string) (player: PlayerData) (items : Inventory) ignoreCooldown = items |> List.map (fun item -> let action = player.Events - |> List.tryFind (fun i -> - match i.Type with + |> List.tryFind (fun event -> + match event.Type with | Hacking h -> h.HackId = item.Id && h.IsInstigator | Shielding id -> id = item.Id | _ -> false) @@ -48,7 +48,7 @@ let constructButtons (actionId: string) (buttonInfo : string) (player: PlayerDat |> Seq.cast let pickDefense actionId player isTrainer = - let shieldItems = player.Inventory |> Inventory.filterByShields + let shieldItems = player.Inventory |> Inventory.getItemsByType ItemType.Shield let buttons = constructButtons actionId (string player.DiscordId) player shieldItems isTrainer let embed = @@ -56,10 +56,10 @@ let pickDefense actionId player isTrainer = .WithTitle("Shield Defense") .WithDescription("Pick a shield to protect yourself from hacks") - for shield in Inventory.getShieldItems player.Inventory do + for shield in Inventory.getShields player.Inventory do let hours = TimeSpan.FromMinutes(int shield.Cooldown).TotalHours let against = WeaponClass.getGoodAgainst(shield.Class) |> snd - embed.AddField(shield.Name, $"Active {hours} hours\nDefeats {against}", true) |> ignore + embed.AddField(shield.Item.Name, $"Active {hours} hours\nDefeats {against}", true) |> ignore DiscordFollowupMessageBuilder() .AddComponents(buttons) @@ -67,7 +67,7 @@ let pickDefense actionId player isTrainer = .AsEphemeral(true) let pickHack actionId attacker defender isTrainer = - let hackItems = attacker.Inventory |> Inventory.filterByHacks + let hackItems = attacker.Inventory |> Inventory.getItemsByType ItemType.Hack let buttons = constructButtons actionId $"{defender.DiscordId}-{defender.Name}" attacker hackItems isTrainer let stealMsg = if not isTrainer then $"{defender.Name} has **{defender.Bank} $GBT** we can take from them. " else "" @@ -77,9 +77,9 @@ let pickHack actionId attacker defender isTrainer = .WithDescription($"{stealMsg}Pick the hack you want to use.") if not isTrainer then - for hack in Inventory.getHackItems attacker.Inventory do + for hack in Inventory.getHacks attacker.Inventory do let amount = if hack.Power > int defender.Bank then int defender.Bank else hack.Power - embed.AddField(hack.Name, $"Cooldown {hack.Cooldown} mins\nExtract {amount} $GBT", true) |> ignore + embed.AddField(hack.Item.Name, $"Cooldown {hack.Cooldown} mins\nExtract {amount} $GBT", true) |> ignore DiscordFollowupMessageBuilder() .AddComponents(buttons) @@ -89,9 +89,9 @@ let pickHack actionId attacker defender isTrainer = let responseSuccessfulHack earnedMoney (targetId : uint64) amountTaken (hack : HackItem) = let embed = DiscordEmbedBuilder() - .WithImageUrl(getItemGif hack.Id) + .WithImageUrl(getItemGif hack.Item.Id) .WithTitle("Hack Attack") - .WithDescription($"You successfully hacked <@{targetId}> using {hack.Name}" + .WithDescription($"You successfully hacked <@{targetId}> using {hack.Item.Name}" + (if earnedMoney then $", and took {amountTaken} 💰$GBT from them!" else "!")) DiscordFollowupMessageBuilder() @@ -99,9 +99,9 @@ let responseSuccessfulHack earnedMoney (targetId : uint64) amountTaken (hack : H .AsEphemeral(true) let responseCreatedShield (shield : ShieldItem) = - let embed = DiscordEmbedBuilder().WithImageUrl(getItemGif shield.Id) + let embed = DiscordEmbedBuilder().WithImageUrl(getItemGif shield.Item.Id) embed.Title <- "Mounted Shield" - embed.Description <- $"Mounted {shield.Name} shield for {TimeSpan.FromMinutes(int shield.Cooldown).TotalHours} hours" + embed.Description <- $"Mounted {shield.Item.Name} shield for {TimeSpan.FromMinutes(int shield.Cooldown).TotalHours} hours" DiscordFollowupMessageBuilder() .AddEmbed(embed) diff --git a/Bot/GameHelpers.fs b/Bot/GameHelpers.fs index a80bdea..5a287fa 100644 --- a/Bot/GameHelpers.fs +++ b/Bot/GameHelpers.fs @@ -3,102 +3,60 @@ namespace Degenz open System open DSharpPlus open DSharpPlus.Entities +open Degenz open Newtonsoft.Json module Armory = - let weapons = +// let weapons : ItemDetails list= [] + let weapons : ItemDetails list = let file = System.IO.File.ReadAllText("Items.json") // let file = System.IO.File.ReadAllText("Bot/Items.json") - JsonConvert.DeserializeObject(file) + JsonConvert.DeserializeObject(file) |> Array.toList module Inventory = - let itemToHack item power hackClass cooldown = { - Id = item.Id - Name = item.Name - Price = item.Price - Power = power - Class = hackClass - Cooldown = cooldown - } - let itemToShield item hackClass cooldown = { - Id = item.Id - Name = item.Name - Price = item.Price - Class = hackClass - Cooldown = cooldown - } - let itemToFood item targetStat boostAmount = { - Id = item.Id - Name = item.Name - Price = item.Price - TargetStat = targetStat - BoostAmount = boostAmount - } - let hackToItem (hack : HackItem) = { - Id = hack.Id - Name = hack.Name - Price = hack.Price - Details = Hack (hack.Power, hack.Class, hack.Cooldown) - } - let shieldToItem (shield : ShieldItem) = { - Id = shield.Id - Name = shield.Name - Price = shield.Price - Details = Shield (shield.Class, shield.Cooldown) - } + let getItemsByType itemType inventory = + match itemType with + | ItemType.Hack -> inventory |> List.filter (fun item -> match item with Hack _ -> true | _ -> false) + | ItemType.Shield -> inventory |> List.filter (fun item -> match item with Shield _ -> true | _ -> false) + | ItemType.Food -> inventory |> List.filter (fun item -> match item with Food _ -> true | _ -> false) + | ItemType.Accessory -> inventory |> List.filter (fun item -> match item with Accessory _ -> true | _ -> false) - let filterByHacks inventory = - inventory |> List.filter (fun item -> match item.Details with Hack _ -> true | _ -> false) - let filterByShields inventory = - inventory |> List.filter (fun item -> match item.Details with Shield _ -> true | _ -> false) - let filterByWeapons inventory = - inventory |> List.filter (fun item -> match item.Details with Hack _ | Shield _ -> true | _ -> false) - let filterByFood inventory = - inventory |> List.filter (fun item -> match item.Details with Food _ -> true | _ -> false) - let filterByAccessories inventory = - inventory |> List.filter (fun item -> match item.Details with Accessory _ -> true | _ -> false) + let findItemById id (inventory : Inventory) = inventory |> List.find (fun item -> item.Id = id) - let getHackItems inventory = - inventory - |> List.choose (fun item -> match item.Details with Hack (p,cl,co) -> Some (itemToHack item p cl co) | _ -> None) - |> List.sortBy (fun item -> item.Id) - let getShieldItems inventory = - inventory - |> List.choose (fun item -> match item.Details with Shield (cl,co) -> Some (itemToShield item cl co) | _ -> None) - |> List.sortBy (fun item -> item.Id) - let getFoodItems inventory = - inventory - |> List.choose (fun item -> match item.Details with Food(t,b) -> Some (itemToFood item t b) | _ -> None) - |> List.sortBy (fun item -> item.Id) - let findItemById id inventory = inventory |> List.find (fun item -> item.Id = id) - let tryFindItemById id inventory = inventory |> List.tryFind (fun item -> item.Id = id) let findHackById id inventory = - inventory |> getHackItems |> List.pick (fun item -> if item.Id = id then Some item else None) + inventory |> List.pick (fun item -> match item with | Hack h -> (if h.Item.Id = id then Some h else None) | _ -> None) let findShieldById id inventory = - inventory |> getShieldItems |> List.pick (fun item -> if item.Id = id then Some item else None) + inventory |> List.pick (fun item -> match item with | Shield s -> (if s.Item.Id = id then Some s else None) | _ -> None) let findFoodById id inventory = - inventory |> getFoodItems |> List.pick (fun item -> if item.Id = id then Some item else None) - let tryFindHackById id inventory = - inventory |> getHackItems |> List.tryFind (fun item -> item.Id = id) - let tryFindShieldById id inventory = - inventory |> getShieldItems |> List.tryFind (fun item -> item.Id = id) + inventory |> List.pick (fun item -> match item with | Food f -> (if f.Item.Id = id then Some f else None) | _ -> None) + let findAccessoryById id inventory = + inventory |> List.pick (fun item -> match item with | Accessory a -> (if a.Item.Id = id then Some a else None) | _ -> None) + + let getHacks inventory = + inventory |> List.choose (fun item -> match item with | Hack h -> Some h | _ -> None) + let getShields inventory = + inventory |> List.choose (fun item -> match item with | Shield s -> Some s | _ -> None) + let getFoods inventory = + inventory |> List.choose (fun item -> match item with | Food f -> Some f | _ -> None) + let getAccessories inventory = + inventory |> List.choose (fun item -> match item with | Accessory a -> Some a | _ -> None) module WeaponClass = - let SameTargetAttackCooldown = System.TimeSpan.FromHours(1) + let SameTargetAttackCooldown = TimeSpan.FromHours(1) let getClassButtonColor item = - match item.Details with - | Hack (_,0,_) | Shield (0,_) -> ButtonStyle.Danger - | Hack (_,1,_) | Shield (1,_) -> ButtonStyle.Primary - | Hack (_,2,_) | Shield (2,_) -> ButtonStyle.Success + match ItemDetails.getClass item with + | 0 -> ButtonStyle.Danger + | 1 -> ButtonStyle.Primary + | 2 -> ButtonStyle.Success | _ -> ButtonStyle.Primary let getClassEmbedColor item = - match item.Details with - | Hack (_,0,_) | Shield (0,_) -> DiscordColor.Red - | Hack (_,1,_) | Shield (1,_) -> DiscordColor.Blurple - | Hack (_,2,_) | Shield (2,_) -> DiscordColor.Green + match ItemDetails.getClass item with + | 0 -> DiscordColor.Red + | 1 -> DiscordColor.Blurple + | 2 -> DiscordColor.Green | _ -> DiscordColor.Blurple let getGoodAgainst = function @@ -118,8 +76,8 @@ module Player = let actions = player.Events |> List.filter (fun (act : PlayerEvent) -> - let cooldown = System.TimeSpan.FromMinutes(int act.Cooldown) - System.DateTime.UtcNow - act.Timestamp < cooldown) + let cooldown = TimeSpan.FromMinutes(int act.Cooldown) + DateTime.UtcNow - act.Timestamp < cooldown) { player with Events = actions } let modifyBank (player : PlayerData) amount = { player with Bank = max (player.Bank + amount) 0 } @@ -138,14 +96,14 @@ module PlayerStats = // let hoursElapsed = (DateTime.UtcNow - lastRead).Hours // let totalDecay = float hoursElapsed * statConfig.BaseDecayRate let modMinMax = - let min = items |> List.sumBy (fun item -> match item.Details with | Accessory(_,floorBoost,_) -> floorBoost | _ -> 0) - let max = items |> List.sumBy (fun item -> match item.Details with | Accessory(_,_,ceilBoost) -> ceilBoost | _ -> 0) + let min = items |> List.sumBy (fun item -> match item with | Accessory a -> a.FloorBoost | _ -> 0) + let max = items |> List.sumBy (fun item -> match item with | Accessory a -> a.CeilBoost | _ -> 0) Range.create (statConfig.BaseRange.Min + min) (statConfig.BaseRange.Max + max) let amountAfterDecay = modMinMax |> Range.constrain amount { Id = statId ; Amount = amountAfterDecay ; ModRange = modMinMax ; LastRead = DateTime.UtcNow } module Arsenal = - let battleItemFormat (items : Item list) = + let battleItemFormat (items : ItemDetails list) = match items with | [] -> "None" | _ -> items |> List.map (fun item -> item.Name) |> String.concat ", " @@ -155,23 +113,23 @@ module Arsenal = | [] -> "None" | acts -> acts - |> List.map (fun act -> - match act.Type with + |> List.map (fun event -> + match event.Type with | Hacking h -> let item = Armory.weapons |> Inventory.findHackById h.HackId - let cooldown = Messaging.getTimeText false WeaponClass.SameTargetAttackCooldown act.Timestamp - $"Hacked {h.Adversary.Name} with {item.Name} {cooldown} ago" + let cooldown = Messaging.getTimeText false WeaponClass.SameTargetAttackCooldown event.Timestamp + $"Hacked {h.Adversary.Name} with {item.Item.Name} {cooldown} ago" | Shielding id -> - let item = Armory.weapons |> Inventory.findHackById id - let cooldown = Messaging.getTimeText true (System.TimeSpan.FromMinutes(int act.Cooldown)) act.Timestamp - $"{item.Name} Shield active for {cooldown}" + let item = Armory.weapons |> Inventory.findShieldById id + let cooldown = Messaging.getTimeText true (TimeSpan.FromMinutes(int event.Cooldown)) event.Timestamp + $"{item.Item.Name} Shield active for {cooldown}" | _ -> "") - |> List.filter (System.String.IsNullOrWhiteSpace >> not) + |> List.filter (String.IsNullOrWhiteSpace >> not) |> String.concat "\n" let statusFormat p = let hacks = Player.getHackEvents p - $"**Hacks:** {Inventory.filterByHacks p.Inventory |> battleItemFormat}\n - **Shields:** {Inventory.filterByShields p.Inventory |> battleItemFormat}\n - **Hack Attacks:**\n{ hacks |> List.take (min hacks.Length 10) |> actionFormat}\n + $"**Hacks:** {Inventory.getItemsByType ItemType.Hack p.Inventory |> battleItemFormat}\n + **Shields:** {Inventory.getItemsByType ItemType.Hack p.Inventory |> battleItemFormat}\n + **Hack Attacks:**\n{hacks |> List.take (min hacks.Length 10) |> actionFormat}\n **Active Shields:**\n{Player.getShieldEvents p |> actionFormat}" diff --git a/Bot/GameTypes.fs b/Bot/GameTypes.fs index 8794e15..90fe63a 100644 --- a/Bot/GameTypes.fs +++ b/Bot/GameTypes.fs @@ -82,40 +82,6 @@ type PlayerEvent = Cooldown : int Timestamp : DateTime } -type HackItem = { - Id : int - Name : string - Price : int - Power : int - Class : int - Cooldown : int -} - -type ShieldItem = { - Id : int - Name : string - Price : int - Class : int - Cooldown : int -} - -type FoodItem = { - Id : int - Name : string - Price : int - TargetStat : StatId - BoostAmount : int -} - -type AccessoryItem = { - Id : int - Name : string - Price : int - TargetStat : StatId - FloorBoost : int - CeilBoost : int -} - [] type ItemType = | Hack @@ -123,20 +89,76 @@ type ItemType = | Food | Accessory -type ItemDetails = - | Hack of power : int * hackClass : int * cooldown : int - | Shield of shieldClass : int * cooldown : int - | Food of targetStat : StatId * boostAmount : int - | Accessory of targetStat : StatId * floorBoost : int * ceilBoost : int -and Item = { +type Item = { Id : int Name : string Price : int -// Type : ItemType - Details : ItemDetails } -and Inventory = Item list -and PlayerData = { + +type HackItem = { + Power : int + Class : int + Cooldown : int + Item : Item +} + +type ShieldItem = { + Class : int + Cooldown : int + Item : Item +} + +type FoodItem = { + TargetStat : StatId + BoostAmount : int + Item : Item +} + +type AccessoryItem = { + TargetStat : StatId + FloorBoost : int + CeilBoost : int + Item : Item +} + +type ItemDetails = + | Hack of HackItem + | Shield of ShieldItem + | Food of FoodItem + | Accessory of AccessoryItem + member this.Id = + match this with + | Hack i -> i.Item.Id + | Shield i -> i.Item.Id + | Food i -> i.Item.Id + | Accessory i -> i.Item.Id + member this.Name = + match this with + | Hack i -> i.Item.Name + | Shield i -> i.Item.Name + | Food i -> i.Item.Name + | Accessory i -> i.Item.Name + member this.Price = + match this with + | Hack i -> i.Item.Price + | Shield i -> i.Item.Price + | Food i -> i.Item.Price + | Accessory i -> i.Item.Price + member this.getItem = + match this with + | Hack i -> i.Item + | Shield i -> i.Item + | Food i -> i.Item + | Accessory i -> i.Item + static member getClass = function + | Hack i -> i.Class + | Shield i -> i.Class + | Food _ -> -1 + | Accessory _ -> -1 + +type Inventory = ItemDetails list + +type PlayerData = { DiscordId : uint64 Name : string Inventory : Inventory diff --git a/Bot/Games/HackerBattle.fs b/Bot/Games/HackerBattle.fs index 62e983e..e5d0826 100644 --- a/Bot/Games/HackerBattle.fs +++ b/Bot/Games/HackerBattle.fs @@ -44,7 +44,7 @@ let checkWeaponHasCooldown (weapon : Item) attacker = | None -> Ok attacker let checkHasEmptyHacks attacker = - match Inventory.getHackItems attacker.Inventory with + match Inventory.getHacks attacker.Inventory with | [] -> Error $"You currently do not have any Hacks to take 💰$GBT from others. Please go to the <#{GuildEnvironment.channelArmory}> and purchase one." | _ -> Ok attacker @@ -74,7 +74,7 @@ let runHackerBattle defender (hack : HackItem) = |> fun p -> p.Events |> List.choose (fun event -> match event.Type with - | Shielding id -> defender.Inventory |> Inventory.getShieldItems |> List.find (fun item -> item.Id = id) |> Some + | Shielding id -> defender.Inventory |> Inventory.getShields |> List.find (fun item -> item.Item.Id = id) |> Some | _ -> None) |> List.map (fun shield -> if hack.Class = shield.Class then Weak else Strong) |> List.contains Weak @@ -84,7 +84,7 @@ let updateCombatants successfulHack (attacker : PlayerData) (defender : PlayerDa { p with Events = attack::p.Events ; Bank = max (p.Bank + amount) 0 } let event isDefenderEvent = let hackEvent = { - HackId = hack.Id + HackId = hack.Item.Id Adversary = if isDefenderEvent then attacker.toDiscordPlayer else defender.toDiscordPlayer IsInstigator = not isDefenderEvent Success = successfulHack @@ -151,7 +151,6 @@ let handleAttack (ctx : IDiscordContext) = let tokens = ctx.GetInteractionId().Split("-") let hackId = int tokens.[1] let hack = Armory.weapons |> Inventory.findHackById hackId - let hackAsItem = Inventory.hackToItem hack let resultId , targetId = UInt64.TryParse tokens.[2] let! resultTarget = DbService.tryFindPlayer targetId @@ -160,8 +159,8 @@ let handleAttack (ctx : IDiscordContext) = do! attacker |> Player.removeExpiredActions |> checkAlreadyHackedTarget defender - >>= checkPlayerOwnsWeapon hackAsItem - >>= checkWeaponHasCooldown hackAsItem + >>= checkPlayerOwnsWeapon hack.Item + >>= checkWeaponHasCooldown hack.Item |> function | Ok atkr -> runHackerBattle defender hack @@ -174,7 +173,7 @@ let handleAttack (ctx : IDiscordContext) = let defend (ctx : IDiscordContext) = executePlayerAction ctx (fun player -> async { - if player.Inventory |> Inventory.filterByShields |> List.length > 0 then + if player.Inventory |> Inventory.getShields |> List.length > 0 then let p = Player.removeExpiredActions player let embed = Embeds.pickDefense "Defend" p false do! ctx.FollowUp embed |> Async.AwaitTask @@ -188,12 +187,11 @@ let handleDefense (ctx : IDiscordContext) = let tokens = ctx.GetInteractionId().Split("-") let shieldId = int tokens.[1] let shield = Armory.weapons |> Inventory.findShieldById shieldId - let shieldAsItem = Inventory.shieldToItem shield do! player - |> checkPlayerOwnsWeapon shieldAsItem + |> checkPlayerOwnsWeapon shield.Item >>= checkPlayerHasShieldSlotsAvailable - >>= checkWeaponHasCooldown shieldAsItem + >>= checkWeaponHasCooldown shield.Item |> handleResultWithResponse ctx (fun p -> async { let embed = Embeds.responseCreatedShield shield do! ctx.FollowUp embed |> Async.AwaitTask @@ -244,6 +242,37 @@ let handleButtonEvent (_ : DiscordClient) (event : ComponentInteractionCreateEve do! eventCtx.Respond(InteractionResponseType.ChannelMessageWithSource, builder) |> Async.AwaitTask } +let invite (ctx : IDiscordContext) = + task { + let channel = ctx.GetGuild().GetChannel(927449884204867664uL) + let invite = channel.CreateInviteAsync(reason = "I MEAN WHY NOT") |> Async.AwaitTask |> Async.RunSynchronously + + printfn "The invite code is %s" invite.Code + + do! ctx.Respond(InteractionResponseType.ChannelMessageWithSource, DiscordInteractionResponseBuilder().AsEphemeral(true).WithContent($"https://discord.gg/{invite.Code}")) |> Async.AwaitTask + } + +//let invite (ctx : IDiscordContext) = +// task { +// let code = Guid.NewGuid().ToString().Substring(0, 7) +// +//// let embed1 = +//// DiscordEmbedBuilder() +//// .WithImageUrl("https://pbs.twimg.com/profile_banners/1449270642340089856/1640071520/1500x500") +// let embed2 = +// DiscordEmbedBuilder() +// .WithDescription($"Send this invite to your friend, when they join, type the `/enter-code` slash command\n\n```{code}```") +// .WithImageUrl("https://pbs.twimg.com/profile_banners/1449270642340089856/1640071520/1500x500") +// .WithTitle("Invite Code") +// +// let msg = +// DiscordInteractionResponseBuilder() +// .AsEphemeral(true) +//// .AddEmbed(embed1) +// .AddEmbed(embed2) +// +// do! ctx.Respond(InteractionResponseType.ChannelMessageWithSource, msg) +// } type HackerGame() = inherit ApplicationCommandModule () @@ -267,6 +296,10 @@ type HackerGame() = do! Messaging.sendSimpleResponse ctx msg } + [] + member this.CreateInvite (ctx : InteractionContext) = + invite (DiscordInteractionContext ctx) + [] member this.Arsenal (ctx : InteractionContext) = enforceChannels (DiscordInteractionContext ctx) (Trainer.handleArsenal) arsenal diff --git a/Bot/Games/Store.fs b/Bot/Games/Store.fs index a80b070..77b0f13 100644 --- a/Bot/Games/Store.fs +++ b/Bot/Games/Store.fs @@ -15,27 +15,27 @@ let getBuyItemsEmbed (playerInventory : Inventory) (storeInventory : Inventory) storeInventory |> List.map (fun item -> let embed = DiscordEmbedBuilder() - match item.Details with - | Hack(power,_,cooldown) -> - embed.AddField($"$GBT Reward |", string power, true) - .AddField("Cooldown |", $"{TimeSpan.FromMinutes(int cooldown).Minutes} minutes", true) + match item with + | Hack hack -> + embed.AddField($"$GBT Reward |", string hack.Power, true) + .AddField("Cooldown |", $"{TimeSpan.FromMinutes(int hack.Cooldown).Minutes} minutes", true) .WithThumbnail(Embeds.getItemIcon item.Id) |> ignore - | Shield(shieldClass,cooldown) -> - embed.AddField($"Strong against |", WeaponClass.getGoodAgainst shieldClass |> snd |> string, true) + | Shield shield -> + embed.AddField($"Strong against |", WeaponClass.getGoodAgainst shield.Class |> snd |> string, true) // .AddField($"Defensive Strength |", string item.Power, true) - .AddField("Active For |", $"{TimeSpan.FromMinutes(int cooldown).Hours} hours", true) + .AddField("Active For |", $"{TimeSpan.FromMinutes(int shield.Cooldown).Hours} hours", true) .WithThumbnail(Embeds.getItemIcon item.Id) |> ignore - | Food(targetStat, boostAmount) -> - embed.AddField($"Stat |", $"{targetStat}", true) - .AddField($"Amount |", $"+{boostAmount}", true) |> ignore - | Accessory(targetStat, floorBoost, ceilBoost) -> - embed.AddField($"Stat |", $"{targetStat}", true) |> ignore - if floorBoost > 0 then - embed.AddField($"Min Boost |", $"+{floorBoost}", true) |> ignore - if ceilBoost > 0 then - embed.AddField($"Max Boost |", $"+{ceilBoost}", true) |> ignore + | Food food -> + embed.AddField($"Stat |", $"{food.TargetStat}", true) + .AddField($"Amount |", $"+{food.BoostAmount}", true) |> ignore + | Accessory accessory -> + embed.AddField($"Stat |", $"{accessory.TargetStat}", true) |> ignore + if accessory.FloorBoost > 0 then + embed.AddField($"Min Boost |", $"+{accessory.FloorBoost}", true) |> ignore + if accessory.CeilBoost > 0 then + embed.AddField($"Max Boost |", $"+{accessory.CeilBoost}", true) |> ignore embed .AddField("Price 💰", (if item.Price = 0 then "Free" else $"{item.Price} $GBT"), true) .WithColor(WeaponClass.getClassEmbedColor item) @@ -53,7 +53,7 @@ let getBuyItemsEmbed (playerInventory : Inventory) (storeInventory : Inventory) .AddComponents(buttons) .AsEphemeral(true) -let getSellEmbed (items : Item list) = +let getSellEmbed (items : ItemDetails list) = let embeds , buttons = items |> List.map (fun item -> @@ -70,7 +70,7 @@ let getSellEmbed (items : Item list) = .AddComponents(buttons) .AsEphemeral(true) -let getConsumeEmbed (items : Item list) = +let getConsumeEmbed (items : ItemDetails list) = let embeds , buttons = items |> List.groupBy (fun item -> item.Id) @@ -78,7 +78,7 @@ let getConsumeEmbed (items : Item list) = let item = List.head items let foodItem = Inventory.findFoodById itemId items DiscordEmbedBuilder() - .AddField($"{foodItem.Name}", $"Total {items.Length}\nBoosts {foodItem.TargetStat} +{foodItem.BoostAmount}", true) + .AddField($"{foodItem.Item.Name}", $"Total {items.Length}\nBoosts {foodItem.TargetStat} +{foodItem.BoostAmount}", true) .WithTitle($"Food Items") .WithColor(WeaponClass.getClassEmbedColor item) .Build() @@ -130,8 +130,8 @@ let handleBuyItem (ctx : IDiscordContext) itemId = executePlayerAction ctx (fun player -> async { let item = Armory.weapons |> Inventory.findItemById itemId do! player - |> checkHasSufficientFunds item - >>= checkAlreadyOwnsItem item + |> checkHasSufficientFunds item.getItem + >>= checkAlreadyOwnsItem item.getItem |> handleResultWithResponse ctx (fun player -> async { let newBalance = player.Bank - item.Price let p = { player with Bank = newBalance ; Inventory = item::player.Inventory } @@ -145,7 +145,7 @@ let handleSell (ctx : IDiscordContext) itemId = let item = Armory.weapons |> Inventory.findItemById itemId do! player - |> checkSoldItemAlready item + |> checkSoldItemAlready item.getItem |> handleResultWithResponse ctx (fun player -> async { let updatedPlayer = { player with @@ -155,7 +155,7 @@ let handleSell (ctx : IDiscordContext) itemId = do! [ DbService.updatePlayer updatedPlayer |> Async.Ignore DbService.removeShieldEvent updatedPlayer.DiscordId itemId |> Async.Ignore - sendFollowUpMessage ctx $"Sold {item.Details} {item.Name} for {item.Price}! Current Balance: {updatedPlayer.Bank}" ] + sendFollowUpMessage ctx $"Sold {item.Name} for {item.Price}! Current Balance: {updatedPlayer.Bank}" ] |> Async.Parallel |> Async.Ignore }) @@ -202,10 +202,10 @@ type Store() = let checkChannel (ctx : IDiscordContext) = match ctx.GetChannel().Id with - | id when id = GuildEnvironment.channelBackAlley -> buy Inventory.filterByHacks ctx - | id when id = GuildEnvironment.channelArmory -> buy Inventory.filterByShields ctx - | id when id = GuildEnvironment.channelMarket -> buy Inventory.filterByFood ctx - | id when id = GuildEnvironment.channelAccessoryShop -> buy Inventory.filterByAccessories ctx + | id when id = GuildEnvironment.channelBackAlley -> buy (Inventory.getItemsByType ItemType.Hack) ctx + | id when id = GuildEnvironment.channelArmory -> buy (Inventory.getItemsByType ItemType.Shield) ctx + | id when id = GuildEnvironment.channelMarket -> buy (Inventory.getItemsByType ItemType.Food) ctx + | id when id = GuildEnvironment.channelAccessoryShop -> buy (Inventory.getItemsByType ItemType.Accessory) ctx | _ -> task { let msg = $"This channel doesn't have any items to sell" @@ -216,22 +216,22 @@ type Store() = member _.BuyItem (ctx : InteractionContext) = checkChannel (DiscordInteractionContext(ctx)) [] - member this.BuyShield (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (buy Inventory.filterByShields) + member this.BuyShield (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (buy (Inventory.getItemsByType ItemType.Shield)) [] - member this.BuyFood (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (buy Inventory.filterByFood) + member this.BuyFood (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (buy (Inventory.getItemsByType ItemType.Food)) [] - member this.SellHack (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (sell "Hacks" Inventory.filterByHacks) + member this.SellHack (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (sell "Hacks" (Inventory.getItemsByType ItemType.Hack)) [] - member this.SellShield (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (sell "Shields" Inventory.filterByShields) + member this.SellShield (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (sell "Shields" (Inventory.getItemsByType ItemType.Shield)) [] member this.Consume (ctx : InteractionContext) = - enforceChannel (DiscordInteractionContext(ctx)) (sell "Shields" Inventory.filterByShields) + enforceChannel (DiscordInteractionContext(ctx)) (sell "Shields" (Inventory.getItemsByType ItemType.Food)) - [] - member this.Inventory (ctx : InteractionContext) = - enforceChannel (DiscordInteractionContext(ctx)) (sell "Shields" Inventory.filterByShields) +// [] +// member this.Inventory (ctx : InteractionContext) = +// enforceChannel (DiscordInteractionContext(ctx)) (sell "Shields" (Inventory.getItemsByType ItemType)) diff --git a/Bot/Games/Trainer.fs b/Bot/Games/Trainer.fs index fe5d53f..a8f33b9 100644 --- a/Bot/Games/Trainer.fs +++ b/Bot/Games/Trainer.fs @@ -19,10 +19,10 @@ let TrainerEvents = [ Adversary = Sensei Success = true IsInstigator = true - HackId = defaultHack.Id } } + HackId = defaultHack.Item.Id } } { Timestamp = System.DateTime.UtcNow Cooldown = defaultShield.Cooldown - Type = Shielding defaultShield.Id } + Type = Shielding defaultShield.Item.Id } ] let sendInitialEmbed (client : DiscordClient) = @@ -52,7 +52,7 @@ let handleTrainerStep1 (ctx : IDiscordContext) = |> Async.AwaitTask let msg = "Beautopia© is a dangerous place... quick, put up a SHIELD 🛡 before another Degen hacks you, and takes your 💰$GBT.\n\n" + "To enable it, you need to run the `/shield` slash command.\n\n" - + $"Type the `/shield` command now, then select - `{defaultShield.Name}`\n" + + $"Type the `/shield` command now, then select - `{defaultShield.Item.Name}`\n" let builder = DiscordInteractionResponseBuilder() .WithContent(msg) @@ -66,7 +66,7 @@ let defend (ctx : IDiscordContext) = do! Messaging.defer ctx let m = ctx.GetDiscordMember() let name = if System.String.IsNullOrEmpty m.Nickname then m.DisplayName else m.Nickname - let embed = Embeds.pickDefense "Trainer-2" { PlayerData.empty with Inventory = [ Inventory.shieldToItem defaultShield ] ; Name = name } true + let embed = Embeds.pickDefense "Trainer-2" { PlayerData.empty with Inventory = [ Shield defaultShield ] ; Name = name } true do! ctx.FollowUp(embed) |> Async.AwaitTask } |> Async.StartAsTask :> Task @@ -88,11 +88,11 @@ let handleDefense (ctx : IDiscordContext) = let embed = Embeds.responseCreatedShield defaultShield do! ctx.FollowUp embed |> Async.AwaitTask do! Async.Sleep 4000 - do! sendMessage' $"Ok, good, let me make sure that worked.\n\nI'll try to **hack** you now with **{defaultHack.Name}**" + do! sendMessage' $"Ok, good, let me make sure that worked.\n\nI'll try to **hack** you now with **{defaultHack.Item.Name}**" do! Async.Sleep 5000 do! sendMessage' $"❌ HACKING FAILED!\n\n{playerName} defended hack from <@{Sensei.Id}>!" do! Async.Sleep 4000 - do! sendFollowUpMessageWithButton ctx (handleDefenseMsg defaultHack.Name) + do! sendFollowUpMessageWithButton ctx (handleDefenseMsg defaultHack.Item.Name) } |> Async.StartAsTask :> Task let handleTrainerStep3 (ctx : IDiscordContext) = @@ -103,7 +103,7 @@ let handleTrainerStep3 (ctx : IDiscordContext) = .WithContent ( "Now let’s **HACK** 💻... I want you to **HACK ME**!\n\n" + "To **hack**, you need to run the `/hack` slash command.\n" - + $"Type the `/hack` command now, then choose me - <@{Sensei.Id}> as your target, and select `{defaultHack.Name}`") + + $"Type the `/hack` command now, then choose me - <@{Sensei.Id}> as your target, and select `{defaultHack.Item.Name}`") do! ctx.Respond(InteractionResponseType.ChannelMessageWithSource, builder) |> Async.AwaitTask } |> Async.StartAsTask :> Task @@ -115,7 +115,7 @@ let hack (target : DiscordUser) (ctx : IDiscordContext) = let isRightTarget = target.Id = Sensei.Id match isRightTarget with | true -> - let player = { PlayerData.empty with Inventory = [ Inventory.hackToItem defaultHack ] } + let player = { PlayerData.empty with Inventory = [ Hack defaultHack ] } let bot = { PlayerData.empty with DiscordId = Sensei.Id ; Name = Sensei.Name } let embed = Embeds.pickHack "Trainer-4" player bot true @@ -151,7 +151,7 @@ let handleHack (ctx : IDiscordContext) = do! DbService.addAchievement player.DiscordId trainerAchievement |> Async.Ignore - sb.Append($"I'm going to gift you a hack,`{defaultHack.Name}` and a shield, `{defaultShield.Name}`") |> ignore + sb.Append($"I'm going to gift you a hack,`{defaultHack.Item.Name}` and a shield, `{defaultShield.Item.Name}`") |> 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 @@ -168,13 +168,13 @@ let handleArsenal (ctx : IDiscordContext) = PlayerInteractions.executePlayerAction ctx (fun player -> async { let hasStockWeapons = player.Inventory - |> List.choose (fun item -> if item.Id = defaultHack.Id || item.Id = defaultShield.Id then Some item else None) + |> List.choose (fun item -> if item.Id = defaultHack.Item.Id || item.Id = defaultShield.Item.Id then Some item else None) |> List.length > 0 let updatedPlayer = if not hasStockWeapons then { Player.removeExpiredActions player with Events = TrainerEvents @ player.Events - Inventory = Inventory.hackToItem defaultHack::Inventory.shieldToItem defaultShield::player.Inventory + Inventory = Hack defaultHack::Shield defaultShield::player.Inventory } else Player.removeExpiredActions player @@ -191,7 +191,7 @@ let handleArsenal (ctx : IDiscordContext) = let! completed = DbService.checkHasAchievement player.DiscordId trainerAchievement if not completed then do! Async.Sleep 3000 - let rewards = [ $"{defaultHack.Name} Hack" ; $"{defaultShield.Name} Shield" ] + let rewards = [ $"{defaultHack.Item.Name} Hack" ; $"{defaultShield.Item.Name} Shield" ] let embed = Embeds.getAchievementEmbed rewards "You completed the Training Dojo and collected loot." trainerAchievement do! ctx.FollowUp(embed) |> Async.AwaitTask do! Async.Sleep 2000 diff --git a/Bot/Items.json b/Bot/Items.json index c99fd23..ff95a07 100644 --- a/Bot/Items.json +++ b/Bot/Items.json @@ -1,177 +1,205 @@ [ { - "Id": 0, - "Name": "Virus", - "Price": 0, - "Type": { - "Case": "Hack", - "Fields": [ - 25, - 0, - 1 - ] - } + "Case": "Hack", + "Fields": [ + { + "Power": 25, + "Class": 0, + "Cooldown": 1, + "Item": { + "Id": 0, + "Name": "Virus", + "Price": 0 + } + } + ] }, { - "Id": 1, - "Name": "Remote Access", - "Price": 500, - "Type": { - "Case": "Hack", - "Fields": [ - 75, - 1, - 3 - ] - } + "Case": "Hack", + "Fields": [ + { + "Power": 75, + "Class": 1, + "Cooldown": 3, + "Item": { + "Id": 1, + "Name": "Remote Access", + "Price": 500 + } + } + ] }, { - "Id": 2, - "Name": "Worm", - "Price": 5000, - "Type": { - "Case": "Hack", - "Fields": [ - 150, - 2, - 5 - ] - } + "Case": "Hack", + "Fields": [ + { + "Power": 150, + "Class": 2, + "Cooldown": 5, + "Item": { + "Id": 2, + "Name": "Worm", + "Price": 5000 + } + } + ] }, { - "Id": 6, - "Name": "Firewall", - "Price": 0, - "Type": { - "Case": "Shield", - "Fields": [ - 0, - 120 - ] - } + "Case": "Shield", + "Fields": [ + { + "Class": 0, + "Cooldown": 120, + "Item": { + "Id": 6, + "Name": "Firewall", + "Price": 0 + } + } + ] }, { - "Id": 7, - "Name": "Encryption", - "Price": 500, - "Type": { - "Case": "Shield", - "Fields": [ - 1, - 240 - ] - } + "Case": "Shield", + "Fields": [ + { + "Class": 1, + "Cooldown": 240, + "Item": { + "Id": 7, + "Name": "Encryption", + "Price": 500 + } + } + ] }, { - "Id": 8, - "Name": "Cypher", - "Price": 5000, - "Type": { - "Case": "Shield", - "Fields": [ - 2, - 380 - ] - } + "Case": "Shield", + "Fields": [ + { + "Class": 2, + "Cooldown": 380, + "Item": { + "Id": 8, + "Name": "Cypher", + "Price": 5000 + } + } + ] }, { - "Id": 12, - "Name": "Protein Powder", - "Price": 50, - "Type": { - "Case": "Food", - "Fields": [ - 0, - 30 - ] - } + "Case": "Food", + "Fields": [ + { + "TargetStat" : 0, + "BoostAmount" : 30, + "Item": { + "Id": 12, + "Name": "Protein Powder", + "Price": 50 + } + } + ] }, { - "Id": 13, - "Name": "Toro Loco", - "Price": 50, - "Type": { - "Case": "Food", - "Fields": [ - 1, - 30 - ] - } + "Case": "Food", + "Fields": [ + { + "TargetStat" : 1, + "BoostAmount" : 30, + "Item": { + "Id": 13, + "Name": "Toro Loco", + "Price": 50 + } + } + ] }, { - "Id": 14, - "Name": "Cigarettes", - "Price": 50, - "Type": { - "Case": "Food", - "Fields": [ - 2, - 30 - ] - } + "Case": "Food", + "Fields": [ + { + "TargetStat" : 2, + "BoostAmount" : 30, + "Item": { + "Id": 14, + "Name": "Oldports Cigs", + "Price": 50 + } + } + ] }, { - "Id": 15, - "Name": "Moon Pie", - "Price": 50, - "Type": { - "Case": "Food", - "Fields": [ - 3, - 30 - ] - } + "Case": "Food", + "Fields": [ + { + "TargetStat" : 3, + "BoostAmount" : 30, + "Item": { + "Id": 15, + "Name": "Moon Pie", + "Price": 50 + } + } + ] }, { - "Id": 20, - "Name": "Kettle Bell", - "Price": 250, - "Type": { - "Case": "Accessory", - "Fields": [ - 0, - 25, - 0 - ] - } + "Case": "Accessory", + "Fields": [ + { + "TargetStat" : 0, + "FloorBoost" : 25, + "CeilBoost" : 0, + "Item": { + "Id": 20, + "Name": "Kettlebell", + "Price": 250 + } + } + ] }, { - "Id": 21, - "Name": "Headphones", - "Price": 250, - "Type": { - "Case": "Accessory", - "Fields": [ - 1, - 25, - 0 - ] - } + "Case": "Accessory", + "Fields": [ + { + "TargetStat" : 1, + "FloorBoost" : 25, + "CeilBoost" : 0, + "Item": { + "Id": 21, + "Name": "Headphones", + "Price": 250 + } + } + ] }, { - "Id": 22, - "Name": "Silk Shirt", - "Price": 250, - "Type": { - "Case": "Accessory", - "Fields": [ - 2, - 0, - 25 - ] - } + "Case": "Accessory", + "Fields": [ + { + "TargetStat" : 2, + "FloorBoost" : 0, + "CeilBoost" : 25, + "Item": { + "Id": 22, + "Name": "Rolox Watch", + "Price": 250 + } + } + ] }, { - "Id": 23, - "Name": "Buddha Keychain", - "Price": 250, - "Type": { - "Case": "Accessory", - "Fields": [ - 3, - 0, - 25 - ] - } + "Case": "Accessory", + "Fields": [ + { + "TargetStat" : 3, + "FloorBoost" : 0, + "CeilBoost" : 25, + "Item": { + "Id": 23, + "Name": "Buddha Keychain", + "Price": 250 + } + } + ] } ] From 64a288bf217f8d19ac7674f7bdbf6a7bf52aa016 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Wed, 2 Mar 2022 23:10:16 +0700 Subject: [PATCH 12/28] Invite tracker --- Bot/Bot.fs | 17 +++++++++++++ Bot/Bot.fsproj | 1 + Bot/InviteTracker.fs | 58 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+) create mode 100644 Bot/InviteTracker.fs diff --git a/Bot/Bot.fs b/Bot/Bot.fs index 93ae670..3d820a4 100644 --- a/Bot/Bot.fs +++ b/Bot/Bot.fs @@ -58,6 +58,18 @@ stealCommands.RegisterCommands(guild); hackerBattleBot.add_ComponentInteractionCreated(AsyncEventHandler(HackerBattle.handleButtonEvent)) storeBot.add_ComponentInteractionCreated(AsyncEventHandler(Store.handleStoreEvents)) stealBot.add_ComponentInteractionCreated(AsyncEventHandler(Thief.handleStealButton)) +hackerBattleBot.add_InviteCreated((fun client args -> + task { + do! InviteTracker.createInvite args.Invite.Inviter.Id args.Invite.Code |> Async.Ignore + })) +hackerBattleBot.add_GuildMemberAdded(AsyncEventHandler(fun client ea -> + task { + let! guildInvites = ea.Guild.GetInvitesAsync() + let! cachedInvites = InviteTracker.getInvites() + for invite in guildInvites do + if invite.Uses < (snd cachedInvites.[invite.Code]) then + do! InviteTracker.addInvitedUser ea.Member.Id invite.Code |> Async.Ignore + })) let asdf (_ : DiscordClient) (event : DSharpPlus.EventArgs.InteractionCreateEventArgs) = async { @@ -86,6 +98,11 @@ GuildEnvironment.botUserArmory <- Some storeBot.CurrentUser stealBot.ConnectAsync() |> Async.AwaitTask |> Async.RunSynchronously +//let channel = hackerBattleBot.GetChannelAsync(1234uL) |> Async.AwaitTask |> Async.RunSynchronously +//channel.invi + + + //async { // let! user = hackerBattleBot.GetUserAsync(GuildEnvironment.botIdHackerBattle) |> Async.AwaitTask // if user <> null then diff --git a/Bot/Bot.fsproj b/Bot/Bot.fsproj index d7929c4..e99922e 100644 --- a/Bot/Bot.fsproj +++ b/Bot/Bot.fsproj @@ -18,6 +18,7 @@ + diff --git a/Bot/InviteTracker.fs b/Bot/InviteTracker.fs new file mode 100644 index 0000000..7a6412e --- /dev/null +++ b/Bot/InviteTracker.fs @@ -0,0 +1,58 @@ +module Degenz.InviteTracker + + +open System +open Npgsql.FSharp + +let connStr = GuildEnvironment.connectionString + +type Invite = { + Code : string + Inviter : uint64 + Count : int +} + +let getInvites () = async { + let! invites = + connStr + |> Sql.connect + |> Sql.query "SELECT code, inviter, count FROM invite" + |> Sql.executeAsync (fun read -> { + Code = read.string "code" + Inviter = read.string "inviter" |> uint64 + Count = read.int "count" + }) + |> Async.AwaitTask + return + invites + |> List.map (fun inv -> (inv.Code , (inv.Inviter , inv.Count))) + |> Map.ofList +} + +let createInvite inviter code = + connStr + |> Sql.connect + |> Sql.parameters [ "code" , Sql.string code ; "inviter" , Sql.string (string inviter) ] + |> Sql.query "INSERT INTO invite (code, inviter) VALUES (@code, @inviter)" + |> Sql.executeNonQueryAsync + |> Async.AwaitTask + +let addInvitedUser did code = + connStr + |> Sql.connect + |> Sql.executeTransactionAsync [ + """ + WITH invite AS (SELECT id FROM invite WHERE code = @code) + INSERT INTO invited_user (discord_id, invite_id) SELECT @discord_id, invite.id FROM invite; + """ , [ [ "@discord_id" , Sql.string (string did) ] ; [ "@code" , Sql.string code ] ] + "UPDATE invite SET count = count + 1 WHERE code = @code" , [ [ "@code" , Sql.string code ] ] + ] + |> Async.AwaitTask + +let getInviteAttributions user = + connStr + |> Sql.connect + |> Sql.parameters [ "did" , Sql.string (string user.DiscordId) ] + |> Sql.query "SELECT sum(count) AS total FROM invite WHERE inviter = @did" + |> Sql.executeAsync (fun read -> read.int "total") + |> Async.AwaitTask From 25f3c0b89daa86aceda10a600655db8250602156 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Thu, 3 Mar 2022 12:43:13 +0700 Subject: [PATCH 13/28] Add helper invite commands --- Bot/Bot.fs | 4 -- Bot/Games/HackerBattle.fs | 86 +++++++++++++++++++++++++++++++++++---- Bot/GuildEnvironment.fs | 1 + Bot/InviteTracker.fs | 22 ++++++++-- 4 files changed, 98 insertions(+), 15 deletions(-) diff --git a/Bot/Bot.fs b/Bot/Bot.fs index 3d820a4..9d9a3d3 100644 --- a/Bot/Bot.fs +++ b/Bot/Bot.fs @@ -58,10 +58,6 @@ stealCommands.RegisterCommands(guild); hackerBattleBot.add_ComponentInteractionCreated(AsyncEventHandler(HackerBattle.handleButtonEvent)) storeBot.add_ComponentInteractionCreated(AsyncEventHandler(Store.handleStoreEvents)) stealBot.add_ComponentInteractionCreated(AsyncEventHandler(Thief.handleStealButton)) -hackerBattleBot.add_InviteCreated((fun client args -> - task { - do! InviteTracker.createInvite args.Invite.Inviter.Id args.Invite.Code |> Async.Ignore - })) hackerBattleBot.add_GuildMemberAdded(AsyncEventHandler(fun client ea -> task { let! guildInvites = ea.Guild.GetInvitesAsync() diff --git a/Bot/Games/HackerBattle.fs b/Bot/Games/HackerBattle.fs index e5d0826..302861a 100644 --- a/Bot/Games/HackerBattle.fs +++ b/Bot/Games/HackerBattle.fs @@ -1,6 +1,7 @@ module Degenz.HackerBattle open System +open System.Text open System.Threading.Tasks open DSharpPlus open DSharpPlus.Entities @@ -242,16 +243,71 @@ let handleButtonEvent (_ : DiscordClient) (event : ComponentInteractionCreateEve do! eventCtx.Respond(InteractionResponseType.ChannelMessageWithSource, builder) |> Async.AwaitTask } -let invite (ctx : IDiscordContext) = +let createInvite (ctx : IDiscordContext) = task { - let channel = ctx.GetGuild().GetChannel(927449884204867664uL) - let invite = channel.CreateInviteAsync(reason = "I MEAN WHY NOT") |> Async.AwaitTask |> Async.RunSynchronously + let channel = ctx.GetGuild().Channels.[GuildEnvironment.channelWelcome] + let! invite = channel.CreateInviteAsync(max_age = 259200) + do! InviteTracker.createInvite (ctx.GetDiscordMember().Id) invite.Code |> Async.Ignore - printfn "The invite code is %s" invite.Code + let embed = + DiscordEmbedBuilder() + .WithDescription($"Send this invite to your friend, when they join, they can type the `/enter-code` slash command") + .WithImageUrl("https://pbs.twimg.com/profile_banners/1449270642340089856/1640071520/1500x500") + .WithTitle("Invite Code") - do! ctx.Respond(InteractionResponseType.ChannelMessageWithSource, DiscordInteractionResponseBuilder().AsEphemeral(true).WithContent($"https://discord.gg/{invite.Code}")) |> Async.AwaitTask + let msg = + DiscordInteractionResponseBuilder() + .AddEmbed(embed) + .AsEphemeral(true) + .WithContent($"https://discord.gg/{invite.Code}") + + do! ctx.Respond(InteractionResponseType.ChannelMessageWithSource, msg) } +let listServerInvites (ctx : IDiscordContext) = task { + let! invites = ctx.GetGuild().GetInvitesAsync() + let sb = StringBuilder() + for invite in invites do + sb.AppendLine($"{invite.Inviter.Username} - {invite.Code}") |> ignore + let msg = + DiscordInteractionResponseBuilder() + .AsEphemeral(true) + .WithContent("Server Invites\n" + sb.ToString()) + do! ctx.Respond(InteractionResponseType.ChannelMessageWithSource, msg) +} + +let getAttributions (ctx : IDiscordContext) userId = task { + let! total = InviteTracker.getInviteAttributions(userId) + let msg = + DiscordInteractionResponseBuilder() + .AsEphemeral(true) + .WithContent($"<@{userId}> has invited {total} people") + do! ctx.Respond(InteractionResponseType.ChannelMessageWithSource, msg) +} + +let getInvitedUsers (ctx : IDiscordContext) userId = task { + let! users = InviteTracker.getInvitedUsers(userId) + let sb = StringBuilder() + for user in users do + sb.AppendLine($"<@{user}>") |> ignore + let msg = + DiscordInteractionResponseBuilder() + .AsEphemeral(true) + .WithContent($"<@{userId}> has invited the following people:\n{sb}") + do! ctx.Respond(InteractionResponseType.ChannelMessageWithSource, msg) +} + +let clearInvites (ctx : IDiscordContext) = task { + let! invites = ctx.GetGuild().GetInvitesAsync() + do! + invites + |> Seq.map (fun invite -> invite.DeleteAsync() |> Async.AwaitTask) + |> Async.Parallel + |> Async.Ignore +} + + + //let invite (ctx : IDiscordContext) = // task { // let code = Guid.NewGuid().ToString().Substring(0, 7) @@ -296,9 +352,25 @@ type HackerGame() = do! Messaging.sendSimpleResponse ctx msg } - [] + [] member this.CreateInvite (ctx : InteractionContext) = - invite (DiscordInteractionContext ctx) + createInvite (DiscordInteractionContext ctx) + + [] + member this.ListServerInvites (ctx : InteractionContext) = + listServerInvites (DiscordInteractionContext ctx) + + [] + member this.getAttributions (ctx : InteractionContext, [] user : DiscordUser) = + getAttributions (DiscordInteractionContext ctx) user.Id + + [] + member this.ListInvitedPeople (ctx : InteractionContext, [] user : DiscordUser) = + getInvitedUsers (DiscordInteractionContext ctx) user.Id + + [] + member this.ClearInvites (ctx : InteractionContext) = + clearInvites (DiscordInteractionContext ctx) [] member this.Arsenal (ctx : InteractionContext) = diff --git a/Bot/GuildEnvironment.fs b/Bot/GuildEnvironment.fs index 0221b09..4dbc87a 100644 --- a/Bot/GuildEnvironment.fs +++ b/Bot/GuildEnvironment.fs @@ -25,6 +25,7 @@ let channelBackAlley = getId "CHANNEL_BACKALLEY" let channelBattle = getId "CHANNEL_BATTLE" let channelMarket = getId "CHANNEL_MARKET" let channelAccessoryShop = getId "CHANNEL_ACCESSORIES" +let channelWelcome = getId "CHANNEL_WELCOME" //let channelThievery = getId "CHANNEL_THIEVERY" let botIdHackerBattle = getId "BOT_HACKER_BATTLE" diff --git a/Bot/InviteTracker.fs b/Bot/InviteTracker.fs index 7a6412e..7eb8795 100644 --- a/Bot/InviteTracker.fs +++ b/Bot/InviteTracker.fs @@ -16,7 +16,10 @@ let getInvites () = async { let! invites = connStr |> Sql.connect - |> Sql.query "SELECT code, inviter, count FROM invite" + |> Sql.query """ + SELECT code, inviter, count FROM invite + WHERE created_at > (current_timestamp at time zone 'utc') - interval '3 day' + """ |> Sql.executeAsync (fun read -> { Code = read.string "code" Inviter = read.string "inviter" |> uint64 @@ -49,10 +52,21 @@ let addInvitedUser did code = ] |> Async.AwaitTask -let getInviteAttributions user = +let getInviteAttributions userId = connStr |> Sql.connect - |> Sql.parameters [ "did" , Sql.string (string user.DiscordId) ] + |> Sql.parameters [ "did" , Sql.string (string userId) ] |> Sql.query "SELECT sum(count) AS total FROM invite WHERE inviter = @did" - |> Sql.executeAsync (fun read -> read.int "total") + |> Sql.executeRowAsync (fun read -> read.int "total") + |> Async.AwaitTask + +let getInvitedUsers userId = + connStr + |> Sql.connect + |> Sql.parameters [ "did" , Sql.string (string userId) ] + |> Sql.query """ + WITH invite AS (SELECT id FROM invite WHERE inviter = @did) + SELECT discord_id FROM invited_user, invite WHERE invite.id = invited_user.invite_id + """ + |> Sql.executeAsync (fun read -> read.string "discord_id" |> uint64) |> Async.AwaitTask From 24f56c046c27341676c7f5e4d37225156191489d Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Thu, 3 Mar 2022 15:58:25 +0700 Subject: [PATCH 14/28] Formatting --- Bot/Games/HackerBattle.fs | 3 ++- Bot/InviteTracker.fs | 23 +++++++++++++---------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/Bot/Games/HackerBattle.fs b/Bot/Games/HackerBattle.fs index 302861a..56784b0 100644 --- a/Bot/Games/HackerBattle.fs +++ b/Bot/Games/HackerBattle.fs @@ -251,7 +251,8 @@ let createInvite (ctx : IDiscordContext) = let embed = DiscordEmbedBuilder() - .WithDescription($"Send this invite to your friend, when they join, they can type the `/enter-code` slash command") + .WithDescription($"Send this invite to your friend, when they join, they can type the `/enter-code` slash command\n\n + ```https://discord.gg/{invite.Code}```") .WithImageUrl("https://pbs.twimg.com/profile_banners/1449270642340089856/1640071520/1500x500") .WithTitle("Invite Code") diff --git a/Bot/InviteTracker.fs b/Bot/InviteTracker.fs index 7eb8795..2f219a0 100644 --- a/Bot/InviteTracker.fs +++ b/Bot/InviteTracker.fs @@ -41,16 +41,19 @@ let createInvite inviter code = |> Async.AwaitTask let addInvitedUser did code = - connStr - |> Sql.connect - |> Sql.executeTransactionAsync [ - """ - WITH invite AS (SELECT id FROM invite WHERE code = @code) - INSERT INTO invited_user (discord_id, invite_id) SELECT @discord_id, invite.id FROM invite; - """ , [ [ "@discord_id" , Sql.string (string did) ] ; [ "@code" , Sql.string code ] ] - "UPDATE invite SET count = count + 1 WHERE code = @code" , [ [ "@code" , Sql.string code ] ] - ] - |> Async.AwaitTask + try + connStr + |> Sql.connect + |> Sql.executeTransactionAsync [ + """ + WITH invite AS (SELECT id FROM invite WHERE code = @code) + INSERT INTO invited_user (discord_id, invite_id) SELECT @discord_id, invite.id FROM invite; + """ , [ [ "@discord_id" , Sql.string (string did) ] ; [ "@code" , Sql.string code ] ] + "UPDATE invite SET count = count + 1 WHERE code = @code" , [ [ "@code" , Sql.string code ] ] + ] + |> Async.AwaitTask + |> Async.Ignore + with _ -> async.Zero () let getInviteAttributions userId = connStr From 6b2b9a86f37c743c758f16d7bc2116a3305aa65e Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Thu, 3 Mar 2022 22:46:51 +0700 Subject: [PATCH 15/28] Setup inviter bot. Fix bug with transaction --- Bot/Bot.fs | 32 ++++---- Bot/Games/HackerBattle.fs | 107 ------------------------ Bot/GuildEnvironment.fs | 2 + Bot/InviteTracker.fs | 168 ++++++++++++++++++++++++++++++++++---- 4 files changed, 170 insertions(+), 139 deletions(-) diff --git a/Bot/Bot.fs b/Bot/Bot.fs index 9d9a3d3..f75e40d 100644 --- a/Bot/Bot.fs +++ b/Bot/Bot.fs @@ -4,9 +4,6 @@ open System.Threading.Tasks open DSharpPlus open DSharpPlus.SlashCommands open Degenz -open Degenz.HackerBattle -open Degenz.Store -open Degenz.Thief open Emzi0767.Utilities //open Degenz.SlotMachine @@ -17,13 +14,11 @@ let guild = GuildEnvironment.guildId let hackerBattleConfig = DiscordConfiguration() let storeConfig = DiscordConfiguration() let stealConfig = DiscordConfiguration() +let inviterConfig = DiscordConfiguration() //let slotMachineConfig = DiscordConfiguration() //hackerBattleConfig.MinimumLogLevel <- Microsoft.Extensions.Logging.LogLevel.Trace //storeConfig.MinimumLogLevel <- Microsoft.Extensions.Logging.LogLevel.Trace -//let configs = [| hackerBattleConfig ; storeConfig ; slotMachineConfig ; |] -let configs = [ hackerBattleConfig ; storeConfig ; stealConfig ] - hackerBattleConfig.TokenType <- TokenType.Bot hackerBattleConfig.Intents <- DiscordIntents.All @@ -33,39 +28,40 @@ storeConfig.Intents <- DiscordIntents.All stealConfig.TokenType <- TokenType.Bot stealConfig.Intents <- DiscordIntents.All +inviterConfig.TokenType <- TokenType.Bot +inviterConfig.Intents <- DiscordIntents.All + hackerBattleConfig.Token <- GuildEnvironment.tokenHackerBattle storeConfig.Token <- GuildEnvironment.tokenStore stealConfig.Token <- GuildEnvironment.tokenSteal +inviterConfig.Token <- GuildEnvironment.tokenInviter //slotMachineConfig.Token <- Environment.GetEnvironmentVariable("BOT_SLOT_MACHINE") let hackerBattleBot = new DiscordClient(hackerBattleConfig) let storeBot = new DiscordClient(storeConfig) let stealBot = new DiscordClient(stealConfig) +let inviterBot = new DiscordClient(inviterConfig) //let slotMachineBot = new DiscordClient(slotMachineConfig) //let clients = [| hackerBattleBot ; storeBot ; slotMachineBot |] let hackerCommands = hackerBattleBot.UseSlashCommands() let storeCommands = storeBot.UseSlashCommands() let stealCommands = stealBot.UseSlashCommands() +let inviterCommands = inviterBot.UseSlashCommands() //let sc3 = slotMachineBot.UseSlashCommands() -hackerCommands.RegisterCommands(guild); -storeCommands.RegisterCommands(guild); -stealCommands.RegisterCommands(guild); +hackerCommands.RegisterCommands(guild); +storeCommands.RegisterCommands(guild); +stealCommands.RegisterCommands(guild); +inviterCommands.RegisterCommands(guild); //hackerCommands.RegisterCommands(guild); //sc3.RegisterCommands(guild); hackerBattleBot.add_ComponentInteractionCreated(AsyncEventHandler(HackerBattle.handleButtonEvent)) storeBot.add_ComponentInteractionCreated(AsyncEventHandler(Store.handleStoreEvents)) stealBot.add_ComponentInteractionCreated(AsyncEventHandler(Thief.handleStealButton)) -hackerBattleBot.add_GuildMemberAdded(AsyncEventHandler(fun client ea -> - task { - let! guildInvites = ea.Guild.GetInvitesAsync() - let! cachedInvites = InviteTracker.getInvites() - for invite in guildInvites do - if invite.Uses < (snd cachedInvites.[invite.Code]) then - do! InviteTracker.addInvitedUser ea.Member.Id invite.Code |> Async.Ignore - })) +inviterBot.add_GuildMemberAdded(AsyncEventHandler(InviteTracker.handleGuildMemberAdded)) +inviterBot.add_GuildMemberRemoved(AsyncEventHandler(InviteTracker.handleGuildMemberRemoved)) let asdf (_ : DiscordClient) (event : DSharpPlus.EventArgs.InteractionCreateEventArgs) = async { @@ -94,6 +90,8 @@ GuildEnvironment.botUserArmory <- Some storeBot.CurrentUser stealBot.ConnectAsync() |> Async.AwaitTask |> Async.RunSynchronously +inviterBot.ConnectAsync() |> Async.AwaitTask |> Async.RunSynchronously + //let channel = hackerBattleBot.GetChannelAsync(1234uL) |> Async.AwaitTask |> Async.RunSynchronously //channel.invi diff --git a/Bot/Games/HackerBattle.fs b/Bot/Games/HackerBattle.fs index 56784b0..413045c 100644 --- a/Bot/Games/HackerBattle.fs +++ b/Bot/Games/HackerBattle.fs @@ -243,93 +243,6 @@ let handleButtonEvent (_ : DiscordClient) (event : ComponentInteractionCreateEve do! eventCtx.Respond(InteractionResponseType.ChannelMessageWithSource, builder) |> Async.AwaitTask } -let createInvite (ctx : IDiscordContext) = - task { - let channel = ctx.GetGuild().Channels.[GuildEnvironment.channelWelcome] - let! invite = channel.CreateInviteAsync(max_age = 259200) - do! InviteTracker.createInvite (ctx.GetDiscordMember().Id) invite.Code |> Async.Ignore - - let embed = - DiscordEmbedBuilder() - .WithDescription($"Send this invite to your friend, when they join, they can type the `/enter-code` slash command\n\n - ```https://discord.gg/{invite.Code}```") - .WithImageUrl("https://pbs.twimg.com/profile_banners/1449270642340089856/1640071520/1500x500") - .WithTitle("Invite Code") - - let msg = - DiscordInteractionResponseBuilder() - .AddEmbed(embed) - .AsEphemeral(true) - .WithContent($"https://discord.gg/{invite.Code}") - - do! ctx.Respond(InteractionResponseType.ChannelMessageWithSource, msg) - } - -let listServerInvites (ctx : IDiscordContext) = task { - let! invites = ctx.GetGuild().GetInvitesAsync() - let sb = StringBuilder() - for invite in invites do - sb.AppendLine($"{invite.Inviter.Username} - {invite.Code}") |> ignore - let msg = - DiscordInteractionResponseBuilder() - .AsEphemeral(true) - .WithContent("Server Invites\n" + sb.ToString()) - do! ctx.Respond(InteractionResponseType.ChannelMessageWithSource, msg) -} - -let getAttributions (ctx : IDiscordContext) userId = task { - let! total = InviteTracker.getInviteAttributions(userId) - let msg = - DiscordInteractionResponseBuilder() - .AsEphemeral(true) - .WithContent($"<@{userId}> has invited {total} people") - do! ctx.Respond(InteractionResponseType.ChannelMessageWithSource, msg) -} - -let getInvitedUsers (ctx : IDiscordContext) userId = task { - let! users = InviteTracker.getInvitedUsers(userId) - let sb = StringBuilder() - for user in users do - sb.AppendLine($"<@{user}>") |> ignore - let msg = - DiscordInteractionResponseBuilder() - .AsEphemeral(true) - .WithContent($"<@{userId}> has invited the following people:\n{sb}") - do! ctx.Respond(InteractionResponseType.ChannelMessageWithSource, msg) -} - -let clearInvites (ctx : IDiscordContext) = task { - let! invites = ctx.GetGuild().GetInvitesAsync() - do! - invites - |> Seq.map (fun invite -> invite.DeleteAsync() |> Async.AwaitTask) - |> Async.Parallel - |> Async.Ignore -} - - - -//let invite (ctx : IDiscordContext) = -// task { -// let code = Guid.NewGuid().ToString().Substring(0, 7) -// -//// let embed1 = -//// DiscordEmbedBuilder() -//// .WithImageUrl("https://pbs.twimg.com/profile_banners/1449270642340089856/1640071520/1500x500") -// let embed2 = -// DiscordEmbedBuilder() -// .WithDescription($"Send this invite to your friend, when they join, type the `/enter-code` slash command\n\n```{code}```") -// .WithImageUrl("https://pbs.twimg.com/profile_banners/1449270642340089856/1640071520/1500x500") -// .WithTitle("Invite Code") -// -// let msg = -// DiscordInteractionResponseBuilder() -// .AsEphemeral(true) -//// .AddEmbed(embed1) -// .AddEmbed(embed2) -// -// do! ctx.Respond(InteractionResponseType.ChannelMessageWithSource, msg) -// } type HackerGame() = inherit ApplicationCommandModule () @@ -353,26 +266,6 @@ type HackerGame() = do! Messaging.sendSimpleResponse ctx msg } - [] - member this.CreateInvite (ctx : InteractionContext) = - createInvite (DiscordInteractionContext ctx) - - [] - member this.ListServerInvites (ctx : InteractionContext) = - listServerInvites (DiscordInteractionContext ctx) - - [] - member this.getAttributions (ctx : InteractionContext, [] user : DiscordUser) = - getAttributions (DiscordInteractionContext ctx) user.Id - - [] - member this.ListInvitedPeople (ctx : InteractionContext, [] user : DiscordUser) = - getInvitedUsers (DiscordInteractionContext ctx) user.Id - - [] - member this.ClearInvites (ctx : InteractionContext) = - clearInvites (DiscordInteractionContext ctx) - [] member this.Arsenal (ctx : InteractionContext) = enforceChannels (DiscordInteractionContext ctx) (Trainer.handleArsenal) arsenal diff --git a/Bot/GuildEnvironment.fs b/Bot/GuildEnvironment.fs index 4dbc87a..25e4073 100644 --- a/Bot/GuildEnvironment.fs +++ b/Bot/GuildEnvironment.fs @@ -18,6 +18,7 @@ let tokenPlayerInteractions = getVar "TOKEN_PLAYER_INTERACTIONS" let tokenSteal = getVar "TOKEN_STEAL" let tokenHackerBattle = getVar "TOKEN_HACKER_BATTLE" let tokenStore = getVar "TOKEN_STORE" +let tokenInviter = getVar "TOKEN_INVITER" let channelEventsHackerBattle = getId "CHANNEL_EVENTS_HACKER_BATTLE" let channelTraining = getId "CHANNEL_TRAINING" let channelArmory = getId "CHANNEL_ARMORY" @@ -30,6 +31,7 @@ let channelWelcome = getId "CHANNEL_WELCOME" //let channelThievery = getId "CHANNEL_THIEVERY" let botIdHackerBattle = getId "BOT_HACKER_BATTLE" let botIdArmory = getId "BOT_ARMORY" +let botInviter = getId "BOT_INVITER" let roleTrainee = getId "ROLE_TRAINEE" let rolePrisoner = getId "ROLE_PRISONER" diff --git a/Bot/InviteTracker.fs b/Bot/InviteTracker.fs index 2f219a0..03fb575 100644 --- a/Bot/InviteTracker.fs +++ b/Bot/InviteTracker.fs @@ -1,7 +1,13 @@ module Degenz.InviteTracker -open System +open System.Text +open System.Threading.Tasks +open DSharpPlus +open DSharpPlus.Entities +open DSharpPlus.EventArgs +open DSharpPlus.SlashCommands +open Degenz.Messaging open Npgsql.FSharp let connStr = GuildEnvironment.connectionString @@ -18,7 +24,7 @@ let getInvites () = async { |> Sql.connect |> Sql.query """ SELECT code, inviter, count FROM invite - WHERE created_at > (current_timestamp at time zone 'utc') - interval '3 day' + WHERE created_at > (current_timestamp at time zone 'utc') - interval '1 day' """ |> Sql.executeAsync (fun read -> { Code = read.string "code" @@ -33,34 +39,50 @@ let getInvites () = async { } let createInvite inviter code = - connStr - |> Sql.connect - |> Sql.parameters [ "code" , Sql.string code ; "inviter" , Sql.string (string inviter) ] - |> Sql.query "INSERT INTO invite (code, inviter) VALUES (@code, @inviter)" - |> Sql.executeNonQueryAsync - |> Async.AwaitTask + connStr + |> Sql.connect + |> Sql.parameters [ "code" , Sql.string code ; "inviter" , Sql.string (string inviter) ] + |> Sql.query "INSERT INTO invite (code, inviter) VALUES (@code, @inviter)" + |> Sql.executeNonQueryAsync + |> Async.AwaitTask + |> Async.Ignore -let addInvitedUser did code = +let addInvitedUser did code count = try connStr |> Sql.connect |> Sql.executeTransactionAsync [ """ - WITH invite AS (SELECT id FROM invite WHERE code = @code) - INSERT INTO invited_user (discord_id, invite_id) SELECT @discord_id, invite.id FROM invite; - """ , [ [ "@discord_id" , Sql.string (string did) ] ; [ "@code" , Sql.string code ] ] - "UPDATE invite SET count = count + 1 WHERE code = @code" , [ [ "@code" , Sql.string code ] ] + INSERT INTO invited_user (discord_id, invite_id) + VALUES (@did, (SELECT id FROM invite WHERE code = @code)); + """ , [ [ "@code" , Sql.string code ; "@did" , Sql.string (string did) ] ] + "UPDATE invite SET count = @count WHERE code = @code" , [ [ "count" , Sql.int count ; "code" , Sql.string code ] ] ] |> Async.AwaitTask |> Async.Ignore with _ -> async.Zero () +let removeInvitedUser did = + try + connStr + |> Sql.connect + |> Sql.parameters [ "did" , Sql.string (string did) ] + |> Sql.query "DELETE FROM invited_user WHERE discord_id = @did" + |> Sql.executeNonQueryAsync + |> Async.AwaitTask + |> Async.Ignore + with _ -> async.Zero () + let getInviteAttributions userId = connStr |> Sql.connect |> Sql.parameters [ "did" , Sql.string (string userId) ] - |> Sql.query "SELECT sum(count) AS total FROM invite WHERE inviter = @did" - |> Sql.executeRowAsync (fun read -> read.int "total") + |> Sql.query """ + SELECT count(*) FROM invited_user + JOIN invite ON invite.id = invited_user.invite_id + WHERE invite.inviter = @did + """ + |> Sql.executeRowAsync (fun read -> read.int "count") |> Async.AwaitTask let getInvitedUsers userId = @@ -73,3 +95,119 @@ let getInvitedUsers userId = """ |> Sql.executeAsync (fun read -> read.string "discord_id" |> uint64) |> Async.AwaitTask + +let createGuildInvite (ctx : IDiscordContext) = + task { + let channel = ctx.GetGuild().Channels.[GuildEnvironment.channelWelcome] + let! invite = channel.CreateInviteAsync(max_age = 86400) + + // When a player generates an invite code but it hasn't expired, it generates the same code, creating a duplicate entry + // so catch the exception thrown because the code column is unique + try + do! createInvite (ctx.GetDiscordMember().Id) invite.Code + with _ -> () + + let embed = + DiscordEmbedBuilder() + .WithDescription($"Use this invite link to earn invite points for future rewards.\nExpires in 1 day. + ```https://discord.gg/{invite.Code}```") + .WithImageUrl("https://pbs.twimg.com/profile_banners/1449270642340089856/1640071520/1500x500") + .WithTitle("Invite Link") + + let msg = + DiscordInteractionResponseBuilder() + .AddEmbed(embed) + .AsEphemeral(true) + .WithContent($"https://discord.gg/{invite.Code}") + + do! ctx.Respond(InteractionResponseType.ChannelMessageWithSource, msg) + } + +let listServerInvites (ctx : IDiscordContext) = task { + let! invites = ctx.GetGuild().GetInvitesAsync() + let sb = StringBuilder() + for invite in invites do + sb.AppendLine($"{invite.Inviter.Username} - {invite.Code}") |> ignore + let msg = + DiscordInteractionResponseBuilder() + .AsEphemeral(true) + .WithContent("Server Invites\n" + sb.ToString()) + do! ctx.Respond(InteractionResponseType.ChannelMessageWithSource, msg) +} + +let getAttributions (ctx : IDiscordContext) userId = task { + let! total = getInviteAttributions(userId) + let msg = + DiscordInteractionResponseBuilder() + .AsEphemeral(true) + .WithContent($"<@{userId}> has invited {total} people") + do! ctx.Respond(InteractionResponseType.ChannelMessageWithSource, msg) +} + +let getInvitedUsersForId (ctx : IDiscordContext) userId = task { + let! users = getInvitedUsers(userId) + let sb = StringBuilder() + for user in users do + sb.AppendLine($"<@{user}>") |> ignore + let msg = + DiscordInteractionResponseBuilder() + .AsEphemeral(true) + .WithContent($"<@{userId}> has invited the following people:\n{sb}") + do! ctx.Respond(InteractionResponseType.ChannelMessageWithSource, msg) +} + +let clearInvites (ctx : IDiscordContext) = task { + let! invites = ctx.GetGuild().GetInvitesAsync() + do! + invites + |> Seq.map (fun invite -> invite.DeleteAsync() |> Async.AwaitTask) + |> Async.Parallel + |> Async.Ignore +} + +let handleGuildMemberAdded _ (eventArgs : GuildMemberAddEventArgs) = + task { + let! guildInvites = eventArgs.Guild.GetInvitesAsync() + let! cachedInvites = getInvites() + for invite in guildInvites do + let result = cachedInvites.TryFind(invite.Code) + match result with + | Some (_,count) -> + if invite.Uses > count then + do! addInvitedUser eventArgs.Member.Id invite.Code invite.Uses |> Async.Ignore + | None -> () + } :> Task + +let handleGuildMemberRemoved _ (eventArgs : GuildMemberRemoveEventArgs) = + task { +// let! guildInvites = eventArgs.Guild.GetInvitesAsync() +// let! cachedInvites = getInvites() +// for invite in guildInvites do +// if invite.Uses < (snd cachedInvites.[invite.Code]) then +// do! addInvitedUser eventArgs.Member.Id invite.Code |> Async.Ignore + return () + } :> Task + +type Inviter() = + inherit ApplicationCommandModule () + + [] + member this.CreateInvite (ctx : InteractionContext) = + createGuildInvite (DiscordInteractionContext ctx) + + [] + member this.ListServerInvites (ctx : InteractionContext) = + listServerInvites (DiscordInteractionContext ctx) + + [] + member this.getAttributions (ctx : InteractionContext, [] user : DiscordUser) = + getAttributions (DiscordInteractionContext ctx) user.Id + + [] + member this.ListInvitedPeople (ctx : InteractionContext, [] user : DiscordUser) = + getInvitedUsersForId (DiscordInteractionContext ctx) user.Id + + [] + member this.ClearInvites (ctx : InteractionContext) = + clearInvites (DiscordInteractionContext ctx) + From 3ef02e43e3de87f3bbbbe60b1a705e14e9f414bf Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 4 Mar 2022 13:11:12 +0700 Subject: [PATCH 16/28] Disable buy items command --- Bot/Games/Store.fs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Bot/Games/Store.fs b/Bot/Games/Store.fs index 77b0f13..0790822 100644 --- a/Bot/Games/Store.fs +++ b/Bot/Games/Store.fs @@ -212,15 +212,18 @@ type Store() = do! Messaging.sendSimpleResponse ctx msg } +// [] +// member _.BuyItem (ctx : InteractionContext) = checkChannel (DiscordInteractionContext(ctx)) +// [] - member _.BuyItem (ctx : InteractionContext) = checkChannel (DiscordInteractionContext(ctx)) + member _.BuyHack (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (buy (Inventory.getItemsByType ItemType.Hack)) [] member this.BuyShield (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (buy (Inventory.getItemsByType ItemType.Shield)) - [] - member this.BuyFood (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (buy (Inventory.getItemsByType ItemType.Food)) - +// [] +// member this.BuyFood (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (buy (Inventory.getItemsByType ItemType.Food)) +// [] member this.SellHack (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (sell "Hacks" (Inventory.getItemsByType ItemType.Hack)) From 6d791d067a18eac54a0290e85e12099207012f41 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 4 Mar 2022 13:19:24 +0700 Subject: [PATCH 17/28] Disable env vars --- Bot/Games/Store.fs | 6 +++--- Bot/GuildEnvironment.fs | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Bot/Games/Store.fs b/Bot/Games/Store.fs index 0790822..7727110 100644 --- a/Bot/Games/Store.fs +++ b/Bot/Games/Store.fs @@ -202,10 +202,10 @@ type Store() = let checkChannel (ctx : IDiscordContext) = match ctx.GetChannel().Id with - | id when id = GuildEnvironment.channelBackAlley -> buy (Inventory.getItemsByType ItemType.Hack) ctx +// | id when id = GuildEnvironment.channelBackAlley -> buy (Inventory.getItemsByType ItemType.Hack) ctx | id when id = GuildEnvironment.channelArmory -> buy (Inventory.getItemsByType ItemType.Shield) ctx - | id when id = GuildEnvironment.channelMarket -> buy (Inventory.getItemsByType ItemType.Food) ctx - | id when id = GuildEnvironment.channelAccessoryShop -> buy (Inventory.getItemsByType ItemType.Accessory) ctx +// | id when id = GuildEnvironment.channelMarket -> buy (Inventory.getItemsByType ItemType.Food) ctx +// | id when id = GuildEnvironment.channelAccessoryShop -> buy (Inventory.getItemsByType ItemType.Accessory) ctx | _ -> task { let msg = $"This channel doesn't have any items to sell" diff --git a/Bot/GuildEnvironment.fs b/Bot/GuildEnvironment.fs index 25e4073..c9210ae 100644 --- a/Bot/GuildEnvironment.fs +++ b/Bot/GuildEnvironment.fs @@ -22,10 +22,10 @@ let tokenInviter = getVar "TOKEN_INVITER" let channelEventsHackerBattle = getId "CHANNEL_EVENTS_HACKER_BATTLE" let channelTraining = getId "CHANNEL_TRAINING" let channelArmory = getId "CHANNEL_ARMORY" -let channelBackAlley = getId "CHANNEL_BACKALLEY" +//let channelBackAlley = getId "CHANNEL_BACKALLEY" let channelBattle = getId "CHANNEL_BATTLE" -let channelMarket = getId "CHANNEL_MARKET" -let channelAccessoryShop = getId "CHANNEL_ACCESSORIES" +//let channelMarket = getId "CHANNEL_MARKET" +//let channelAccessoryShop = getId "CHANNEL_ACCESSORIES" let channelWelcome = getId "CHANNEL_WELCOME" //let channelThievery = getId "CHANNEL_THIEVERY" From 90f01710af3106eeee809408cfc6e015784f87df Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 4 Mar 2022 13:27:44 +0700 Subject: [PATCH 18/28] Another one --- Bot/GuildEnvironment.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Bot/GuildEnvironment.fs b/Bot/GuildEnvironment.fs index c9210ae..140057d 100644 --- a/Bot/GuildEnvironment.fs +++ b/Bot/GuildEnvironment.fs @@ -31,7 +31,7 @@ let channelWelcome = getId "CHANNEL_WELCOME" //let channelThievery = getId "CHANNEL_THIEVERY" let botIdHackerBattle = getId "BOT_HACKER_BATTLE" let botIdArmory = getId "BOT_ARMORY" -let botInviter = getId "BOT_INVITER" +//let botInviter = getId "BOT_INVITER" let roleTrainee = getId "ROLE_TRAINEE" let rolePrisoner = getId "ROLE_PRISONER" From c2f0125fb6ca7a071e0d437d798ec41f9a56d95f Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 4 Mar 2022 15:22:22 +0700 Subject: [PATCH 19/28] Fix slash commands --- Bot/Games/Store.fs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Bot/Games/Store.fs b/Bot/Games/Store.fs index 7727110..bc2e1c2 100644 --- a/Bot/Games/Store.fs +++ b/Bot/Games/Store.fs @@ -215,10 +215,10 @@ type Store() = // [] // member _.BuyItem (ctx : InteractionContext) = checkChannel (DiscordInteractionContext(ctx)) // - [] + [] member _.BuyHack (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (buy (Inventory.getItemsByType ItemType.Hack)) - [] + [] member this.BuyShield (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (buy (Inventory.getItemsByType ItemType.Shield)) // [] From 123cbce2aa150799e8dd01ae870518258613ca50 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 4 Mar 2022 15:32:15 +0700 Subject: [PATCH 20/28] Print debugging :/ --- Bot/InviteTracker.fs | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/Bot/InviteTracker.fs b/Bot/InviteTracker.fs index 03fb575..5d336be 100644 --- a/Bot/InviteTracker.fs +++ b/Bot/InviteTracker.fs @@ -39,13 +39,12 @@ let getInvites () = async { } let createInvite inviter code = - connStr - |> Sql.connect - |> Sql.parameters [ "code" , Sql.string code ; "inviter" , Sql.string (string inviter) ] - |> Sql.query "INSERT INTO invite (code, inviter) VALUES (@code, @inviter)" - |> Sql.executeNonQueryAsync - |> Async.AwaitTask - |> Async.Ignore + connStr + |> Sql.connect + |> Sql.parameters [ "code" , Sql.string code ; "inviter" , Sql.string (string inviter) ] + |> Sql.query "INSERT INTO invite (code, inviter) VALUES (@code, @inviter)" + |> Sql.executeNonQueryAsync + |> Async.AwaitTask let addInvitedUser did code count = try @@ -104,8 +103,12 @@ let createGuildInvite (ctx : IDiscordContext) = // When a player generates an invite code but it hasn't expired, it generates the same code, creating a duplicate entry // so catch the exception thrown because the code column is unique try - do! createInvite (ctx.GetDiscordMember().Id) invite.Code - with _ -> () + let! result = createInvite (ctx.GetDiscordMember().Id) invite.Code + printfn "%A" result + return () + with ex -> + printfn "%A" ex.Message + () let embed = DiscordEmbedBuilder() From 3f4d785bcb6df5092295be8ad32ced3f5c893e0f Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 4 Mar 2022 15:44:46 +0700 Subject: [PATCH 21/28] Maybe the channel doesn't exist --- Bot/InviteTracker.fs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Bot/InviteTracker.fs b/Bot/InviteTracker.fs index 5d336be..3ca7a16 100644 --- a/Bot/InviteTracker.fs +++ b/Bot/InviteTracker.fs @@ -97,8 +97,11 @@ let getInvitedUsers userId = let createGuildInvite (ctx : IDiscordContext) = task { - let channel = ctx.GetGuild().Channels.[GuildEnvironment.channelWelcome] + printfn $"{GuildEnvironment.channelWelcome}" + let ( result , channel ) = ctx.GetGuild().Channels.TryGetValue(GuildEnvironment.channelWelcome) + printfn $"{result} {channel.Name}" let! invite = channel.CreateInviteAsync(max_age = 86400) + printfn $"{invite.Code}" // When a player generates an invite code but it hasn't expired, it generates the same code, creating a duplicate entry // so catch the exception thrown because the code column is unique From 4ef8243e2217f55f53af8bd874e9e48d70c640cf Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 4 Mar 2022 15:54:25 +0700 Subject: [PATCH 22/28] Remove messages --- Bot/InviteTracker.fs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Bot/InviteTracker.fs b/Bot/InviteTracker.fs index 3ca7a16..0e21487 100644 --- a/Bot/InviteTracker.fs +++ b/Bot/InviteTracker.fs @@ -97,17 +97,13 @@ let getInvitedUsers userId = let createGuildInvite (ctx : IDiscordContext) = task { - printfn $"{GuildEnvironment.channelWelcome}" - let ( result , channel ) = ctx.GetGuild().Channels.TryGetValue(GuildEnvironment.channelWelcome) - printfn $"{result} {channel.Name}" + let channel = ctx.GetGuild().Channels.[GuildEnvironment.channelWelcome] let! invite = channel.CreateInviteAsync(max_age = 86400) - printfn $"{invite.Code}" // When a player generates an invite code but it hasn't expired, it generates the same code, creating a duplicate entry // so catch the exception thrown because the code column is unique try - let! result = createInvite (ctx.GetDiscordMember().Id) invite.Code - printfn "%A" result + let! _ = createInvite (ctx.GetDiscordMember().Id) invite.Code return () with ex -> printfn "%A" ex.Message From 3e7cb47438912da222886b13492e2d42c47c1bd7 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 4 Mar 2022 16:57:07 +0700 Subject: [PATCH 23/28] Add unique flag --- Bot/InviteTracker.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Bot/InviteTracker.fs b/Bot/InviteTracker.fs index 0e21487..b4084c2 100644 --- a/Bot/InviteTracker.fs +++ b/Bot/InviteTracker.fs @@ -98,7 +98,7 @@ let getInvitedUsers userId = let createGuildInvite (ctx : IDiscordContext) = task { let channel = ctx.GetGuild().Channels.[GuildEnvironment.channelWelcome] - let! invite = channel.CreateInviteAsync(max_age = 86400) + let! invite = channel.CreateInviteAsync(max_age = 86400, unique = true) // When a player generates an invite code but it hasn't expired, it generates the same code, creating a duplicate entry // so catch the exception thrown because the code column is unique From 3775ff5a44772655936ab4c0603b9e1ac96259f4 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 4 Mar 2022 20:37:15 +0700 Subject: [PATCH 24/28] Remove invited user when they leave the server --- Bot/InviteTracker.fs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Bot/InviteTracker.fs b/Bot/InviteTracker.fs index b4084c2..c2cd05d 100644 --- a/Bot/InviteTracker.fs +++ b/Bot/InviteTracker.fs @@ -182,12 +182,7 @@ let handleGuildMemberAdded _ (eventArgs : GuildMemberAddEventArgs) = let handleGuildMemberRemoved _ (eventArgs : GuildMemberRemoveEventArgs) = task { -// let! guildInvites = eventArgs.Guild.GetInvitesAsync() -// let! cachedInvites = getInvites() -// for invite in guildInvites do -// if invite.Uses < (snd cachedInvites.[invite.Code]) then -// do! addInvitedUser eventArgs.Member.Id invite.Code |> Async.Ignore - return () + do! removeInvitedUser eventArgs.Member.Id } :> Task type Inviter() = From 81cd471a7f661b47d8f7ec5cd889ae65b85a9c2f Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 5 Mar 2022 00:05:03 +0700 Subject: [PATCH 25/28] TODOs --- Bot/Embeds.fs | 1 + Bot/Games/HackerBattle.fs | 1 + Bot/Games/Trainer.fs | 1 + 3 files changed, 3 insertions(+) diff --git a/Bot/Embeds.fs b/Bot/Embeds.fs index b4d8e28..8c847ad 100644 --- a/Bot/Embeds.fs +++ b/Bot/Embeds.fs @@ -57,6 +57,7 @@ let pickDefense actionId player isTrainer = .WithDescription("Pick a shield to protect yourself from hacks") for shield in Inventory.getShields player.Inventory do + // TODO: This is returning a decimal so look into why let hours = TimeSpan.FromMinutes(int shield.Cooldown).TotalHours let against = WeaponClass.getGoodAgainst(shield.Class) |> snd embed.AddField(shield.Item.Name, $"Active {hours} hours\nDefeats {against}", true) |> ignore diff --git a/Bot/Games/HackerBattle.fs b/Bot/Games/HackerBattle.fs index 413045c..5051617 100644 --- a/Bot/Games/HackerBattle.fs +++ b/Bot/Games/HackerBattle.fs @@ -81,6 +81,7 @@ let runHackerBattle defender (hack : HackItem) = |> List.contains Weak let updateCombatants successfulHack (attacker : PlayerData) (defender : PlayerData) (hack : HackItem) prize = + // TODO: Look into the prizes and how we're handling them because it seems it can be negative let updatePlayer amount attack p = { p with Events = attack::p.Events ; Bank = max (p.Bank + amount) 0 } let event isDefenderEvent = diff --git a/Bot/Games/Trainer.fs b/Bot/Games/Trainer.fs index a8f33b9..3ce6dfd 100644 --- a/Bot/Games/Trainer.fs +++ b/Bot/Games/Trainer.fs @@ -167,6 +167,7 @@ let handleHack (ctx : IDiscordContext) = let handleArsenal (ctx : IDiscordContext) = PlayerInteractions.executePlayerAction ctx (fun player -> async { let hasStockWeapons = + // TODO: This didn't seem to work and someone was gifted both weapons when they already owned one player.Inventory |> List.choose (fun item -> if item.Id = defaultHack.Item.Id || item.Id = defaultShield.Item.Id then Some item else None) |> List.length > 0 From eba589746bd069319790fe54e65c53ad926608df Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 5 Mar 2022 19:55:54 +0700 Subject: [PATCH 26/28] Fix a few bugs and disable inviter --- Bot/Bot.fs | 2 +- Bot/Embeds.fs | 3 +- Bot/GameHelpers.fs | 4 +-- Bot/Games/HackerBattle.fs | 4 +-- Bot/Games/Thief.fs | 2 +- Bot/Games/Trainer.fs | 71 ++++++++++++++++++++++++--------------- Bot/Items.json | 2 +- 7 files changed, 51 insertions(+), 37 deletions(-) diff --git a/Bot/Bot.fs b/Bot/Bot.fs index f75e40d..b49ff4b 100644 --- a/Bot/Bot.fs +++ b/Bot/Bot.fs @@ -90,7 +90,7 @@ GuildEnvironment.botUserArmory <- Some storeBot.CurrentUser stealBot.ConnectAsync() |> Async.AwaitTask |> Async.RunSynchronously -inviterBot.ConnectAsync() |> Async.AwaitTask |> Async.RunSynchronously +//inviterBot.ConnectAsync() |> Async.AwaitTask |> Async.RunSynchronously //let channel = hackerBattleBot.GetChannelAsync(1234uL) |> Async.AwaitTask |> Async.RunSynchronously //channel.invi diff --git a/Bot/Embeds.fs b/Bot/Embeds.fs index 8c847ad..e764d66 100644 --- a/Bot/Embeds.fs +++ b/Bot/Embeds.fs @@ -57,8 +57,7 @@ let pickDefense actionId player isTrainer = .WithDescription("Pick a shield to protect yourself from hacks") for shield in Inventory.getShields player.Inventory do - // TODO: This is returning a decimal so look into why - let hours = TimeSpan.FromMinutes(int shield.Cooldown).TotalHours + let hours = TimeSpan.FromMinutes(int shield.Cooldown).TotalHours |> int let against = WeaponClass.getGoodAgainst(shield.Class) |> snd embed.AddField(shield.Item.Name, $"Active {hours} hours\nDefeats {against}", true) |> ignore diff --git a/Bot/GameHelpers.fs b/Bot/GameHelpers.fs index 5a287fa..fb1c120 100644 --- a/Bot/GameHelpers.fs +++ b/Bot/GameHelpers.fs @@ -43,7 +43,7 @@ module Inventory = inventory |> List.choose (fun item -> match item with | Accessory a -> Some a | _ -> None) module WeaponClass = - let SameTargetAttackCooldown = TimeSpan.FromHours(1) + let SameTargetAttackCooldown = TimeSpan.FromHours(2) let getClassButtonColor item = match ItemDetails.getClass item with @@ -130,6 +130,6 @@ module Arsenal = let statusFormat p = let hacks = Player.getHackEvents p $"**Hacks:** {Inventory.getItemsByType ItemType.Hack p.Inventory |> battleItemFormat}\n - **Shields:** {Inventory.getItemsByType ItemType.Hack p.Inventory |> battleItemFormat}\n + **Shields:** {Inventory.getItemsByType ItemType.Shield p.Inventory |> battleItemFormat}\n **Hack Attacks:**\n{hacks |> List.take (min hacks.Length 10) |> actionFormat}\n **Active Shields:**\n{Player.getShieldEvents p |> actionFormat}" diff --git a/Bot/Games/HackerBattle.fs b/Bot/Games/HackerBattle.fs index 5051617..db0228a 100644 --- a/Bot/Games/HackerBattle.fs +++ b/Bot/Games/HackerBattle.fs @@ -65,7 +65,7 @@ let checkPlayerHasShieldSlotsAvailable player = | false -> Ok updatedPlayer let checkTargetHasFunds target player = - match target.Bank = 0 with + match target.Bank <= 0 with | true -> Error $"Looks like the poor bastard has no $GBT... pick a different victim." | false -> Ok player @@ -81,7 +81,6 @@ let runHackerBattle defender (hack : HackItem) = |> List.contains Weak let updateCombatants successfulHack (attacker : PlayerData) (defender : PlayerData) (hack : HackItem) prize = - // TODO: Look into the prizes and how we're handling them because it seems it can be negative let updatePlayer amount attack p = { p with Events = attack::p.Events ; Bank = max (p.Bank + amount) 0 } let event isDefenderEvent = @@ -162,6 +161,7 @@ let handleAttack (ctx : IDiscordContext) = |> Player.removeExpiredActions |> checkAlreadyHackedTarget defender >>= checkPlayerOwnsWeapon hack.Item + >>= checkTargetHasFunds defender >>= checkWeaponHasCooldown hack.Item |> function | Ok atkr -> diff --git a/Bot/Games/Thief.fs b/Bot/Games/Thief.fs index 233cc7d..57f2c49 100644 --- a/Bot/Games/Thief.fs +++ b/Bot/Games/Thief.fs @@ -82,7 +82,7 @@ let checkVictimStealingCooldown defender attacker = | None -> Ok attacker let checkTargetHasFunds target player = - match target.Bank = 0 with + match target.Bank <= 0 with | true -> Error $"Looks like the poor bastard has no $GBT... pick a different victim." | false -> Ok player diff --git a/Bot/Games/Trainer.fs b/Bot/Games/Trainer.fs index 3ce6dfd..dbebebd 100644 --- a/Bot/Games/Trainer.fs +++ b/Bot/Games/Trainer.fs @@ -12,18 +12,21 @@ let Sensei = { Id = GuildEnvironment.botIdHackerBattle ; Name = "Sensei" } let defaultHack = Armory.weapons |> Inventory.findHackById (int ItemId.Virus) let defaultShield = Armory.weapons |> Inventory.findShieldById (int ItemId.Firewall) -let TrainerEvents = [ - { Timestamp = System.DateTime.UtcNow - Cooldown = defaultHack.Cooldown +let HackEvent = { + Timestamp = System.DateTime.UtcNow + Cooldown = 1 Type = Hacking { Adversary = Sensei Success = true IsInstigator = true - HackId = defaultHack.Item.Id } } - { Timestamp = System.DateTime.UtcNow + HackId = defaultHack.Item.Id + } +} +let ShieldEvent = { + Timestamp = System.DateTime.UtcNow Cooldown = defaultShield.Cooldown - Type = Shielding defaultShield.Item.Id } -] + Type = Shielding defaultShield.Item.Id +} let sendInitialEmbed (client : DiscordClient) = async { @@ -166,27 +169,39 @@ let handleHack (ctx : IDiscordContext) = let handleArsenal (ctx : IDiscordContext) = PlayerInteractions.executePlayerAction ctx (fun player -> async { - let hasStockWeapons = - // TODO: This didn't seem to work and someone was gifted both weapons when they already owned one - player.Inventory - |> List.choose (fun item -> if item.Id = defaultHack.Item.Id || item.Id = defaultShield.Item.Id then Some item else None) - |> List.length > 0 - let updatedPlayer = - if not hasStockWeapons then { - Player.removeExpiredActions player with - Events = TrainerEvents @ player.Events - Inventory = Hack defaultHack::Shield defaultShield::player.Inventory - } - else - Player.removeExpiredActions player - if not hasStockWeapons then - do! - [ DbService.addPlayerEvent player.DiscordId TrainerEvents.[0] - DbService.addPlayerEvent player.DiscordId TrainerEvents.[1] - DbService.updatePlayer updatedPlayer ] - |> Async.Parallel - |> Async.Ignore - let embed = Embeds.getArsenalEmbed updatedPlayer + let hack = + if player.Inventory |> List.exists (fun i -> i.Id = defaultHack.Item.Id) + then [] + else [ Hack defaultHack ] + let shield = + if player.Inventory |> List.exists (fun i -> i.Id = defaultShield.Item.Id) + then [] + else [ Shield defaultShield ] + let shieldEvent = + let hasShield = + player + |> Player.removeExpiredActions + |> fun p -> p.Events + |> List.exists (fun e -> match e.Type with Shielding shieldId -> shieldId = defaultShield.Item.Id | _ -> false) + if hasShield + then [] + else [ ShieldEvent ] + let updatedPlayer = { + Player.removeExpiredActions player with + Events = shieldEvent @ player.Events + Inventory = hack @ shield @ player.Inventory + } + if not (List.isEmpty hack) || not (List.isEmpty shield) then + do! DbService.updatePlayer updatedPlayer |> Async.Ignore + if not (List.isEmpty shieldEvent) then + do! DbService.addPlayerEvent player.DiscordId (List.head shieldEvent) |> Async.Ignore + + let playerForEmbed = { + player with + Events = [ HackEvent ; ShieldEvent ] + Inventory = hack @ shield @ player.Inventory + } + let embed = Embeds.getArsenalEmbed playerForEmbed do! ctx.FollowUp(embed) |> Async.AwaitTask let! completed = DbService.checkHasAchievement player.DiscordId trainerAchievement diff --git a/Bot/Items.json b/Bot/Items.json index ff95a07..f4a7c1f 100644 --- a/Bot/Items.json +++ b/Bot/Items.json @@ -77,7 +77,7 @@ "Fields": [ { "Class": 2, - "Cooldown": 380, + "Cooldown": 360, "Item": { "Id": 8, "Name": "Cypher", From 5b962bac2e6b202d46147c72ccdabcb5472bdd09 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 5 Mar 2022 21:52:04 +0700 Subject: [PATCH 27/28] Basic bot pausing --- Bot/Bot.fs | 45 ++++++++++++++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/Bot/Bot.fs b/Bot/Bot.fs index b49ff4b..b951662 100644 --- a/Bot/Bot.fs +++ b/Bot/Bot.fs @@ -1,5 +1,7 @@ module Degenz.Bot +open System.IO +open System.IO.Pipes open System.Threading.Tasks open DSharpPlus open DSharpPlus.SlashCommands @@ -88,28 +90,41 @@ GuildEnvironment.botUserHackerBattle <- Some hackerBattleBot.CurrentUser storeBot.ConnectAsync() |> Async.AwaitTask |> Async.RunSynchronously GuildEnvironment.botUserArmory <- Some storeBot.CurrentUser -stealBot.ConnectAsync() |> Async.AwaitTask |> Async.RunSynchronously +//stealBot.ConnectAsync() |> Async.AwaitTask |> Async.RunSynchronously //inviterBot.ConnectAsync() |> Async.AwaitTask |> Async.RunSynchronously -//let channel = hackerBattleBot.GetChannelAsync(1234uL) |> Async.AwaitTask |> Async.RunSynchronously -//channel.invi +let rec loop areBotsRunning = + async { + if not (File.Exists "fsharp-bots") then + use file = File.Create "fsharp-bots" + file.Flush() + let! file = File.ReadAllTextAsync("fsharp-bots") |> Async.AwaitTask -//async { -// let! user = hackerBattleBot.GetUserAsync(GuildEnvironment.botIdHackerBattle) |> Async.AwaitTask -// if user <> null then -// GuildEnvironment.botUserHackerBattle <- Some user -// return () -//} |> Async.RunSynchronously + let! ran = + async { + if areBotsRunning && file.StartsWith "kill" then + printfn "Disconnecting bots" + do! hackerBattleBot.DisconnectAsync() |> Async.AwaitTask + do! storeBot.DisconnectAsync() |> Async.AwaitTask + return false + elif not areBotsRunning && not (file.StartsWith "kill") then + printfn "Reconnecting bots" + do! hackerBattleBot.ConnectAsync() |> Async.AwaitTask + do! storeBot.ConnectAsync() |> Async.AwaitTask + return true + else + return areBotsRunning + } + + do! Async.Sleep 3000 + return! loop (ran) + } + +Async.Start (loop true) -//async { -// let! user = storeBot.GetUserAsync(GuildEnvironment.botIdHackerBattle) |> Async.AwaitTask -// if user <> null then -// GuildEnvironment.botUserHackerBattle <- Some user -// return () -//} |> Async.RunSynchronously Task.Delay(-1) |> Async.AwaitTask From eeedf9eccd6de413f374d8c5fe9c04d4116b6f2a Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 5 Mar 2022 22:35:41 +0700 Subject: [PATCH 28/28] Fix a few bugs --- Bot/Bot.fs | 4 ++-- Bot/Games/Store.fs | 2 +- Bot/Games/Trainer.fs | 14 +++++++++----- Bot/GuildEnvironment.fs | 4 ++-- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/Bot/Bot.fs b/Bot/Bot.fs index b951662..b8e4ba1 100644 --- a/Bot/Bot.fs +++ b/Bot/Bot.fs @@ -81,8 +81,8 @@ let asdf (_ : DiscordClient) (event : DSharpPlus.EventArgs.InteractionCreateEven :> Task //hackerBattleBot.add_InteractionCreated(AsyncEventHandler(asdf)) -if guild <> 922419263275425832uL then - Trainer.sendInitialEmbed hackerBattleBot +//if guild <> 922419263275425832uL then +// Trainer.sendInitialEmbed hackerBattleBot hackerBattleBot.ConnectAsync() |> Async.AwaitTask |> Async.RunSynchronously GuildEnvironment.botUserHackerBattle <- Some hackerBattleBot.CurrentUser diff --git a/Bot/Games/Store.fs b/Bot/Games/Store.fs index bc2e1c2..eac30da 100644 --- a/Bot/Games/Store.fs +++ b/Bot/Games/Store.fs @@ -62,7 +62,7 @@ let getSellEmbed (items : ItemDetails list) = .WithTitle($"{item.Name}") .WithColor(WeaponClass.getClassEmbedColor item) .Build() - , DiscordButtonComponent(WeaponClass.getClassButtonColor item, $"Sell-{id}", $"Sell {item.Name}") :> DiscordComponent) + , DiscordButtonComponent(WeaponClass.getClassButtonColor item, $"Sell-{item.Id}", $"Sell {item.Name}") :> DiscordComponent) |> List.unzip DiscordFollowupMessageBuilder() diff --git a/Bot/Games/Trainer.fs b/Bot/Games/Trainer.fs index dbebebd..2459bdd 100644 --- a/Bot/Games/Trainer.fs +++ b/Bot/Games/Trainer.fs @@ -12,7 +12,7 @@ let Sensei = { Id = GuildEnvironment.botIdHackerBattle ; Name = "Sensei" } let defaultHack = Armory.weapons |> Inventory.findHackById (int ItemId.Virus) let defaultShield = Armory.weapons |> Inventory.findShieldById (int ItemId.Firewall) -let HackEvent = { +let HackEvent () = { Timestamp = System.DateTime.UtcNow Cooldown = 1 Type = Hacking { @@ -22,7 +22,7 @@ let HackEvent = { HackId = defaultHack.Item.Id } } -let ShieldEvent = { +let ShieldEvent () = { Timestamp = System.DateTime.UtcNow Cooldown = defaultShield.Cooldown Type = Shielding defaultShield.Item.Id @@ -185,7 +185,7 @@ let handleArsenal (ctx : IDiscordContext) = |> List.exists (fun e -> match e.Type with Shielding shieldId -> shieldId = defaultShield.Item.Id | _ -> false) if hasShield then [] - else [ ShieldEvent ] + else [ ShieldEvent() ] let updatedPlayer = { Player.removeExpiredActions player with Events = shieldEvent @ player.Events @@ -194,11 +194,15 @@ let handleArsenal (ctx : IDiscordContext) = if not (List.isEmpty hack) || not (List.isEmpty shield) then do! DbService.updatePlayer updatedPlayer |> Async.Ignore if not (List.isEmpty shieldEvent) then - do! DbService.addPlayerEvent player.DiscordId (List.head shieldEvent) |> Async.Ignore + try + do! DbService.addPlayerEvent player.DiscordId (List.head shieldEvent) |> Async.Ignore + with ex -> + printfn "%s" ex.Message + () let playerForEmbed = { player with - Events = [ HackEvent ; ShieldEvent ] + Events = [ HackEvent() ; ShieldEvent() ] Inventory = hack @ shield @ player.Inventory } let embed = Embeds.getArsenalEmbed playerForEmbed diff --git a/Bot/GuildEnvironment.fs b/Bot/GuildEnvironment.fs index 140057d..c6047d5 100644 --- a/Bot/GuildEnvironment.fs +++ b/Bot/GuildEnvironment.fs @@ -4,8 +4,8 @@ module Degenz.GuildEnvironment open System open DSharpPlus.Entities open dotenv.net -DotEnv.Load(DotEnvOptions(envFilePaths = [ "../../../../.dev.env" ], overwriteExistingVars = false)) -//DotEnv.Load(DotEnvOptions(envFilePaths = [ "../../../../.stag.env" ], overwriteExistingVars = false)) +//DotEnv.Load(DotEnvOptions(envFilePaths = [ "../../../../.dev.env" ], overwriteExistingVars = false)) +DotEnv.Load(DotEnvOptions(envFilePaths = [ "../../../../.stag.env" ], overwriteExistingVars = false)) //DotEnv.Load(DotEnvOptions(envFilePaths = [ "../../../../.prod.env" ], overwriteExistingVars = false)) let getVar str = Environment.GetEnvironmentVariable(str)