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 } -