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