diff --git a/Bot/Bot.fs b/Bot/Bot.fs index dfb89e2..b8e4ba1 100644 --- a/Bot/Bot.fs +++ b/Bot/Bot.fs @@ -1,12 +1,11 @@ module Degenz.Bot +open System.IO +open System.IO.Pipes 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,11 +16,10 @@ 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 - -//let configs = [| hackerBattleConfig ; storeConfig ; slotMachineConfig ; |] -let configs = [ hackerBattleConfig ; storeConfig ; stealConfig ] +//storeConfig.MinimumLogLevel <- Microsoft.Extensions.Logging.LogLevel.Trace hackerBattleConfig.TokenType <- TokenType.Bot hackerBattleConfig.Intents <- DiscordIntents.All @@ -32,31 +30,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)) +inviterBot.add_GuildMemberAdded(AsyncEventHandler(InviteTracker.handleGuildMemberAdded)) +inviterBot.add_GuildMemberRemoved(AsyncEventHandler(InviteTracker.handleGuildMemberRemoved)) let asdf (_ : DiscordClient) (event : DSharpPlus.EventArgs.InteractionCreateEventArgs) = async { @@ -74,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 @@ -85,32 +92,38 @@ GuildEnvironment.botUserArmory <- Some storeBot.CurrentUser //stealBot.ConnectAsync() |> Async.AwaitTask |> Async.RunSynchronously -//async { -// let! user = hackerBattleBot.GetUserAsync(GuildEnvironment.botIdHackerBattle) |> Async.AwaitTask -// if user <> null then -// GuildEnvironment.botUserHackerBattle <- Some user -// return () -//} |> Async.RunSynchronously +//inviterBot.ConnectAsync() |> Async.AwaitTask |> Async.RunSynchronously -//async { -// let! user = storeBot.GetUserAsync(GuildEnvironment.botIdHackerBattle) |> Async.AwaitTask -// if user <> null then -// GuildEnvironment.botUserHackerBattle <- Some user -// return () -//} |> Async.RunSynchronously -if guild = 922419263275425832uL then - let interactionsConfig = DiscordConfiguration() - interactionsConfig.TokenType <- TokenType.Bot - interactionsConfig.Intents <- DiscordIntents.All - interactionsConfig.Token <- GuildEnvironment.tokenPlayerInteractions +let rec loop areBotsRunning = + async { + if not (File.Exists "fsharp-bots") then + use file = File.Create "fsharp-bots" + file.Flush() - let interactionsBot = new DiscordClient(interactionsConfig) + let! file = File.ReadAllTextAsync("fsharp-bots") |> Async.AwaitTask - let commands = interactionsBot.UseSlashCommands() - commands.RegisterCommands(guild) + 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 + } - interactionsBot.ConnectAsync() |> Async.AwaitTask |> Async.RunSynchronously + do! Async.Sleep 3000 + return! loop (ran) + } + +Async.Start (loop true) Task.Delay(-1) diff --git a/Bot/Bot.fsproj b/Bot/Bot.fsproj index 6d23b5c..e99922e 100644 --- a/Bot/Bot.fsproj +++ b/Bot/Bot.fsproj @@ -11,21 +11,23 @@ PreserveNewest + - - + + + + + + - - - - - - + + + + + + - - - \ No newline at end of file diff --git a/DbService/DbService.fs b/Bot/DbService.fs similarity index 61% rename from DbService/DbService.fs rename to Bot/DbService.fs index de6f76d..31de54e 100644 --- a/DbService/DbService.fs +++ b/Bot/DbService.fs @@ -1,32 +1,26 @@ module Degenz.DbService -open System.Security.Cryptography.X509Certificates -open Degenz.Types - open System open Npgsql.FSharp +open Degenz + +let connStr = GuildEnvironment.connectionString type User = { Name : string DiscordId : uint64 Bank : int + Inventory : int list Strength : int - Inventory : int array + Focus : int + Charisma : int + Luck : int } -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)) - Events = [||] - Traits = { PlayerTraits.empty with Strength = user.Strength } - 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 @@ -50,59 +44,94 @@ 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" - }) - |> 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 -> + let inv = read.intArray "inventory" + { + DiscordId = read.string "discord_id" |> uint64 + Name = read.string "display_name" + Bank = read.int "gbt" * 1 + Inventory = inv |> Array.toList + Strength = read.int "strength" + Focus = read.int "focus" + Charisma = read.int "charisma" + 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 (int player.Traits.Strength) - "inv", Sql.intArray (player.Inventory |> Array.map (fun i -> i.Id)) - ] - |> Sql.query """ - UPDATE "user" SET gbt = @gbt, strength = @str, inventory = @inv + "inv", Sql.intArray (player.Inventory |> Array.ofList |> Array.map (fun item -> item.Id)) + "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 @@ -116,7 +145,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 @@ -133,7 +162,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 @@ -146,7 +175,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/Embeds.fs b/Bot/Embeds.fs index be37bb7..e764d66 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 @@ -9,43 +8,37 @@ 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 : Inventory) ignoreCooldown = + items + |> List.map (fun item -> let action = player.Events - |> Array.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) - let btnColor = Game.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}") @@ -55,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.Inventory |> Inventory.getItemsByType ItemType.Shield + 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 = Game.getGoodAgainst(s.Class) |> snd - embed.AddField(s.Name, $"Active {hours} hours\nDefeats {against}", true) |> ignore + for shield in Inventory.getShields player.Inventory do + 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 DiscordFollowupMessageBuilder() .AddComponents(buttons) @@ -73,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.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 "" let embed = @@ -82,31 +77,31 @@ 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 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.Item.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(getHackGif (enum(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() .AddEmbed(embed.Build()) .AsEphemeral(true) -let responseCreatedShield (shield : Item) = - let embed = DiscordEmbedBuilder().WithImageUrl(getShieldGif (enum(shield.Id))) +let responseCreatedShield (shield : ShieldItem) = + let embed = DiscordEmbedBuilder().WithImageUrl(getItemGif shield.Item.Id) embed.Title <- "Mounted Shield" - embed.Description <- $"Mounted {shield.Name} shield for {TimeSpan.FromMinutes(int shield.Cooldown).Hours} hours" + embed.Description <- $"Mounted {shield.Item.Name} shield for {TimeSpan.FromMinutes(int shield.Cooldown).TotalHours} hours" DiscordFollowupMessageBuilder() .AddEmbed(embed) @@ -116,66 +111,6 @@ 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 embeds , buttons = - store - |> Array.filter (fun i -> i.Type = itemType) - |> Array.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 -// .AddField($"Defensive Strength |", string item.Power, true) - .AddField($"Strong against |", Game.getGoodAgainst item.Class |> snd |> string, true) - .AddField("Active For |", $"{TimeSpan.FromMinutes(int item.Cooldown).Hours} hours", true) - .WithThumbnail(getShieldIcon (enum(item.Id))) - |> ignore - embed - .AddField("Price 💰", (if item.Price = 0 then "Free" else $"{item.Price} $GBT"), true) - .WithColor(Game.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}") - embed.Build() , button :> DiscordComponent) - |> Array.unzip - - DiscordFollowupMessageBuilder() - .AddEmbeds(embeds) - .AddComponents(buttons) - .AsEphemeral(true) - -let getSellEmbed (itemType : ItemType) (player : PlayerData) = - 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 - .AddField("Sell For 💰", $"{item.Price} $GBT", true) - .WithTitle($"{item.Name}") - .WithColor(Game.getClassEmbedColor item.Class) - |> ignore - let button = DiscordButtonComponent(Game.getClassButtonColor item.Class, $"Sell-{item.Id}", $"Sell {item.Name}") - embed.Build() , button :> DiscordComponent) - |> Array.unzip - - DiscordFollowupMessageBuilder() - .AddEmbeds(embeds) - .AddComponents(buttons) - .AsEphemeral(true) - let getArsenalEmbed (player : PlayerData) = DiscordFollowupMessageBuilder() .AsEphemeral(true) diff --git a/Bot/Game.fs b/Bot/Game.fs deleted file mode 100644 index 9ccd028..0000000 --- a/Bot/Game.fs +++ /dev/null @@ -1,125 +0,0 @@ -namespace Degenz - -open System.Threading.Tasks -open DSharpPlus -open DSharpPlus.Entities -open Degenz.DbService -open Degenz.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 ) - - 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 - 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..fb1c120 --- /dev/null +++ b/Bot/GameHelpers.fs @@ -0,0 +1,135 @@ +namespace Degenz + +open System +open DSharpPlus +open DSharpPlus.Entities +open Degenz +open Newtonsoft.Json + +module Armory = +// 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) + |> Array.toList + +module Inventory = + 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 findItemById id (inventory : Inventory) = inventory |> List.find (fun item -> item.Id = id) + + let findHackById id inventory = + 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 |> 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 |> 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 = TimeSpan.FromHours(2) + + let getClassButtonColor item = + match ItemDetails.getClass item with + | 0 -> ButtonStyle.Danger + | 1 -> ButtonStyle.Primary + | 2 -> ButtonStyle.Success + | _ -> ButtonStyle.Primary + + let getClassEmbedColor item = + match ItemDetails.getClass item with + | 0 -> DiscordColor.Red + | 1 -> DiscordColor.Blurple + | 2 -> DiscordColor.Green + | _ -> DiscordColor.Blurple + + let getGoodAgainst = function + | 0 -> ( ItemId.Firewall , ItemId.Virus ) + | 1 -> ( ItemId.Encryption , ItemId.RemoteAccess ) + | _ -> ( ItemId.Cypher , ItemId.Worm ) + +module Player = + let getHackEvents player = + player.Events + |> List.filter (fun act -> match act.Type with PlayerEventType.Hacking h -> h.IsInstigator | _ -> false) + let getShieldEvents player = + player.Events + |> List.filter (fun act -> match act.Type with PlayerEventType.Shielding _ -> true | _ -> false) + + let removeExpiredActions player = + let actions = + player.Events + |> List.filter (fun (act : PlayerEvent) -> + 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 } + +module PlayerStats = + // 4.17f would go from 100 to 0 in roughly 24 hours + 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 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 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 : ItemDetails list) = + match items with + | [] -> "None" + | _ -> items |> List.map (fun item -> item.Name) |> String.concat ", " + + let actionFormat (actions : PlayerEvent List) = + match actions with + | [] -> "None" + | acts -> + acts + |> 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 event.Timestamp + $"Hacked {h.Adversary.Name} with {item.Item.Name} {cooldown} ago" + | Shielding id -> + 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 (String.IsNullOrWhiteSpace >> not) + |> String.concat "\n" + + let statusFormat p = + let hacks = Player.getHackEvents p + $"**Hacks:** {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/GameTypes.fs b/Bot/GameTypes.fs new file mode 100644 index 0000000..90fe63a --- /dev/null +++ b/Bot/GameTypes.fs @@ -0,0 +1,180 @@ +[] +module Degenz.Types + +open System +open Degenz + +[] +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 + | Worm = 2 + | Firewall = 6 + | Encryption = 7 + | Cypher = 8 + | ProteinPowder = 12 + | ToroLoco = 13 + | Cigs = 14 + | MoonPie = 15 + +type StatId = + | Strength = 0 + | Focus = 1 + | Charisma = 2 + | Luck = 3 + +type StatConfig = { + Id : StatId + BaseDecayRate : float + BaseRange : Range +} + +type PlayerStat = { + Id : StatId + Amount : int + ModRange : Range + LastRead : DateTime +} +with static member empty = { Id = StatId.Strength ; Amount = 0 ; ModRange = Range.normalized ; 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 + | 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 ItemType = + | Hack + | Shield + | Food + | Accessory + +type Item = { + Id : int + Name : string + Price : int +} + +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 + Events : PlayerEvent list + Stats : Stats + Bank : int +} +// Achievements : string array +// XP : int +with member this.toDiscordPlayer = { Id = this.DiscordId ; Name = this.Name } + static member empty = + { DiscordId = 0uL + Name = "None" + Inventory = [] + Events = [] + Stats = Stats.empty +// Achievements = [||] +// XP = 0 + Bank = 0 } diff --git a/Bot/HackerBattle.fs b/Bot/Games/HackerBattle.fs similarity index 78% rename from Bot/HackerBattle.fs rename to Bot/Games/HackerBattle.fs index 27b870f..db0228a 100644 --- a/Bot/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 @@ -8,6 +9,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 @@ -18,7 +20,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) @@ -31,7 +33,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 @@ -43,68 +45,63 @@ let checkWeaponHasCooldown (weapon : Item) attacker = | None -> Ok 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." + 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 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 + match defenses |> List.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 |> 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 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 -let calculateDamage (hack : Item) (shield : Item) = - if hack.Class = shield.Class - then Weak - else Strong - -let runHackerBattle defender hack = +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 -> Armory.battleItems |> Array.find (fun w -> w.Id = id) |> Some + | Shielding id -> defender.Inventory |> Inventory.getShields |> List.find (fun item -> item.Item.Id = id) |> Some | _ -> None) - |> Array.map (calculateDamage hack) - |> 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 : Item) prize = +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 - Adversary = if isDefenderEvent then attacker.basicPlayer else defender.basicPlayer + HackId = hack.Item.Id + Adversary = if isDefenderEvent then attacker.toDiscordPlayer else defender.toDiscordPlayer IsInstigator = not isDefenderEvent Success = successfulHack } { 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 - 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 -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 do! updateCombatants true attacker defender hack (prizeAmount * 1) @@ -119,7 +116,7 @@ let successfulHack (ctx : IDiscordContext) attacker defender hack = |> Async.Ignore } -let failedHack (ctx : IDiscordContext) attacker defender hack = +let failedHack (ctx : IDiscordContext) attacker defender (hack : HackItem) = async { 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!" @@ -136,7 +133,7 @@ let failedHack (ctx : IDiscordContext) attacker defender hack = } let hack (target : DiscordUser) (ctx : IDiscordContext) = - Game.executePlayerActionWithTarget target ctx (fun attacker defender -> async { + executePlayerActionWithTarget target ctx (fun attacker defender -> async { do! attacker |> Player.removeExpiredActions |> checkAlreadyHackedTarget defender @@ -151,20 +148,21 @@ let hack (target : DiscordUser) (ctx : IDiscordContext) = }) let handleAttack (ctx : IDiscordContext) = - Game.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 + let hack = Armory.weapons |> Inventory.findHackById hackId 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 -> do! attacker |> Player.removeExpiredActions |> checkAlreadyHackedTarget defender - >>= checkPlayerOwnsWeapon hack - >>= checkWeaponHasCooldown hack + >>= checkPlayerOwnsWeapon hack.Item + >>= checkTargetHasFunds defender + >>= checkWeaponHasCooldown hack.Item |> function | Ok atkr -> runHackerBattle defender hack @@ -176,8 +174,8 @@ let handleAttack (ctx : IDiscordContext) = }) let defend (ctx : IDiscordContext) = - Game.executePlayerAction ctx (fun player -> async { - if Player.getShields player |> Array.length > 0 then + executePlayerAction ctx (fun player -> async { + 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 @@ -187,15 +185,15 @@ let defend (ctx : IDiscordContext) = }) let handleDefense (ctx : IDiscordContext) = - Game.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 + let shield = Armory.weapons |> Inventory.findShieldById shieldId do! player - |> checkPlayerOwnsWeapon shield - >>= checkPlayerHasShieldSlotsAvailable shield - >>= checkWeaponHasCooldown shield + |> checkPlayerOwnsWeapon shield.Item + >>= checkPlayerHasShieldSlotsAvailable + >>= checkWeaponHasCooldown shield.Item |> handleResultWithResponse ctx (fun p -> async { let embed = Embeds.responseCreatedShield shield do! ctx.FollowUp embed |> Async.AwaitTask @@ -204,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 @@ -218,7 +216,7 @@ let handleDefense (ctx : IDiscordContext) = }) let arsenal (ctx : IDiscordContext) = - Game.executePlayerAction ctx (fun player -> async { + executePlayerAction ctx (fun player -> async { let updatedPlayer = Player.removeExpiredActions player let builder = DiscordFollowupMessageBuilder() let embed = DiscordEmbedBuilder() @@ -226,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/RockPaperScissors.fs b/Bot/Games/RockPaperScissors.fs similarity index 96% rename from Bot/RockPaperScissors.fs rename to Bot/Games/RockPaperScissors.fs index 38e9487..8183495 100644 --- a/Bot/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 @@ -85,7 +84,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 +99,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/Games/SlotMachine.fs similarity index 89% rename from Bot/SlotMachine.fs rename to Bot/Games/SlotMachine.fs index 5c1a2be..d40a1b3 100644 --- a/Bot/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 @@ -15,7 +14,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)] @@ -24,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 new file mode 100644 index 0000000..eac30da --- /dev/null +++ b/Bot/Games/Store.fs @@ -0,0 +1,240 @@ +module Degenz.Store + +open System +open System.Threading.Tasks +open DSharpPlus.Entities +open DSharpPlus +open DSharpPlus.EventArgs +open DSharpPlus.SlashCommands +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 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 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 shield.Cooldown).Hours} hours", true) + .WithThumbnail(Embeds.getItemIcon item.Id) + |> 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) + .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 : ItemDetails 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-{item.Id}", $"Sell {item.Name}") :> DiscordComponent) + |> List.unzip + + DiscordFollowupMessageBuilder() + .AddEmbeds(embeds) + .AddComponents(buttons) + .AsEphemeral(true) + +let getConsumeEmbed (items : ItemDetails 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.Item.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 + 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 |> List.exists (fun w -> item.Id = w.Id) + then Error $"You already own {item.Name}!" + else Ok player + +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 inventory! Looks like you sold it already." + +let checkHasItemsInArsenal itemType items player = + if List.isEmpty items |> not + then Ok player + else Error $"You currently have no {itemType} in your arsenal to sell!" + +let buy getItems (ctx : IDiscordContext) = + executePlayerAction ctx (fun player -> async { + let itemStore = getBuyItemsEmbed (getItems player.Inventory) (getItems Armory.weapons) + do! ctx.FollowUp itemStore |> Async.AwaitTask + }) + +let sell itemType getItems (ctx : IDiscordContext) = + executePlayerAction ctx (fun player -> async { + let items = getItems player.Inventory + match checkHasItemsInArsenal itemType items player with + | Ok _ -> let itemStore = getSellEmbed items + do! ctx.FollowUp(itemStore) |> Async.AwaitTask + | Error e -> do! sendFollowUpMessage ctx e + }) + +// 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.weapons |> Inventory.findItemById itemId + do! player + |> 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 } + do! DbService.updatePlayer p |> Async.Ignore + do! sendFollowUpMessage ctx $"Successfully purchased {item.Name}! You now have {newBalance} 💰$GBT remaining" + }) + }) + +let handleSell (ctx : IDiscordContext) itemId = + executePlayerAction ctx (fun player -> async { + let item = Armory.weapons |> Inventory.findItemById itemId + do! + player + |> checkSoldItemAlready item.getItem + |> handleResultWithResponse ctx (fun player -> async { + let updatedPlayer = { + player with + Bank = player.Bank + item.Price + Inventory = player.Inventory |> List.filter (fun i -> i.Id <> itemId) + } + do! + [ DbService.updatePlayer updatedPlayer |> Async.Ignore + DbService.removeShieldEvent updatedPlayer.DiscordId itemId |> Async.Ignore + sendFollowUpMessage ctx $"Sold {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() + let itemId = int <| id.Split("-").[1] + match id with + | id when id.StartsWith("Buy") -> handleBuyItem ctx itemId + | id when id.StartsWith("Sell") -> handleSell ctx itemId + | _ -> + task { + let builder = DiscordInteractionResponseBuilder() + builder.IsEphemeral <- true + builder.Content <- $"Incorrect Action identifier {id}" + do! ctx.Respond(InteractionResponseType.ChannelMessageWithSource, builder) |> Async.AwaitTask + } + +type Store() = + inherit ApplicationCommandModule () + + let enforceChannel (ctx : IDiscordContext) (storeFn : IDiscordContext -> Task) = + match ctx.GetChannel().Id with + | id when id = GuildEnvironment.channelArmory -> storeFn ctx + | _ -> + task { + let msg = $"You must go to <#{GuildEnvironment.channelArmory}> channel to buy or sell weapons" + do! Messaging.sendSimpleResponse ctx msg + } + + let checkChannel (ctx : IDiscordContext) = + match ctx.GetChannel().Id with +// | 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" + do! Messaging.sendSimpleResponse ctx msg + } + +// [] +// 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.SellHack (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (sell "Hacks" (Inventory.getItemsByType ItemType.Hack)) + + [] + 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.getItemsByType ItemType.Food)) + +// [] +// member this.Inventory (ctx : InteractionContext) = +// enforceChannel (DiscordInteractionContext(ctx)) (sell "Shields" (Inventory.getItemsByType ItemType)) + diff --git a/Bot/Thief.fs b/Bot/Games/Thief.fs similarity index 89% rename from Bot/Thief.fs rename to Bot/Games/Thief.fs index 12e5975..57f2c49 100644 --- a/Bot/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) @@ -71,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 -> @@ -81,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 @@ -94,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) @@ -119,7 +120,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! + executePlayerActionWithTarget target ctx (fun thief victim -> async { do! thief |> checkPlayerIsAttackingThemselves victim // |> checkVictimStealingCooldown victim @@ -127,7 +128,7 @@ 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.Amount victim.Stats.Strength.Amount let chance = int (winPercentage * 100.0) let buttons = @@ -137,7 +138,7 @@ 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 + 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" @@ -160,7 +161,7 @@ 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 + 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()) @@ -174,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 = { @@ -190,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) @@ -205,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) @@ -219,7 +220,7 @@ let handleSteal (ctx : IDiscordContext) = } if answer = "yes" then let targetId = uint64 tokens.[2] - Game.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/Trainer.fs b/Bot/Games/Trainer.fs similarity index 70% rename from Bot/Trainer.fs rename to Bot/Games/Trainer.fs index a2dcc91..2459bdd 100644 --- a/Bot/Trainer.fs +++ b/Bot/Games/Trainer.fs @@ -4,27 +4,29 @@ open System.Text open System.Threading.Tasks open DSharpPlus open DSharpPlus.Entities -open DSharpPlus.EventArgs open Degenz.Types 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 defaultHack = Armory.weapons |> Inventory.findHackById (int ItemId.Virus) +let defaultShield = Armory.weapons |> Inventory.findShieldById (int ItemId.Firewall) -let TrainerEvents = [| - { Timestamp = System.DateTime.UtcNow - Cooldown = 2 +let HackEvent () = { + Timestamp = System.DateTime.UtcNow + Cooldown = 1 Type = Hacking { Adversary = Sensei Success = true IsInstigator = true - HackId = defaultHack.Id } } - { Timestamp = System.DateTime.UtcNow - Cooldown = defaultShield.Cooldown - Type = Shielding defaultShield.Id } -|] + HackId = defaultHack.Item.Id + } +} +let ShieldEvent () = { + Timestamp = System.DateTime.UtcNow + Cooldown = defaultShield.Cooldown + Type = Shielding defaultShield.Item.Id +} let sendInitialEmbed (client : DiscordClient) = async { @@ -53,7 +55,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) @@ -67,7 +69,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 = [ Shield defaultShield ] ; Name = name } true do! ctx.FollowUp(embed) |> Async.AwaitTask } |> Async.StartAsTask :> Task @@ -84,17 +86,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 **{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) = @@ -105,7 +106,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 @@ -117,8 +118,9 @@ let hack (target : DiscordUser) (ctx : IDiscordContext) = let isRightTarget = target.Id = Sensei.Id match isRightTarget with | true -> + 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" { PlayerData.empty with Inventory = [| defaultHack |] } bot true + let embed = Embeds.pickHack "Trainer-4" player bot true do! ctx.FollowUp(embed) |> Async.AwaitTask | false -> @@ -131,7 +133,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 @@ -147,12 +149,12 @@ 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 + 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 @@ -166,30 +168,50 @@ let handleHack (ctx : IDiscordContext) = }) let handleArsenal (ctx : IDiscordContext) = - Game.executePlayerAction ctx (fun player -> async { - let hasStockWeapons = Player.getHacks player |> Array.exists (fun item -> item.Id = defaultHack.Id) - let updatedPlayer = - if not hasStockWeapons then { - Player.removeExpiredActions player with - Events = TrainerEvents |> Array.append player.Events - Inventory = [| defaultHack ; defaultShield |] |> Array.append player.Inventory - } - else - 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 ] - |> Async.Parallel - |> Async.Ignore - let embed = Embeds.getArsenalEmbed updatedPlayer + PlayerInteractions.executePlayerAction ctx (fun player -> async { + 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 + try + do! DbService.addPlayerEvent player.DiscordId (List.head shieldEvent) |> Async.Ignore + with ex -> + printfn "%s" ex.Message + () + + 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 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" ] + 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/GuildEnvironment.fs b/Bot/GuildEnvironment.fs index df3c973..c6047d5 100644 --- a/Bot/GuildEnvironment.fs +++ b/Bot/GuildEnvironment.fs @@ -4,27 +4,34 @@ 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) 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" 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" +//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" 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 new file mode 100644 index 0000000..c2cd05d --- /dev/null +++ b/Bot/InviteTracker.fs @@ -0,0 +1,210 @@ +module Degenz.InviteTracker + + +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 + +type Invite = { + Code : string + Inviter : uint64 + Count : int +} + +let getInvites () = async { + let! invites = + connStr + |> Sql.connect + |> Sql.query """ + SELECT code, inviter, count FROM invite + WHERE created_at > (current_timestamp at time zone 'utc') - interval '1 day' + """ + |> 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 count = + try + connStr + |> Sql.connect + |> Sql.executeTransactionAsync [ + """ + 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 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 = + 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 + +let createGuildInvite (ctx : IDiscordContext) = + task { + let channel = ctx.GetGuild().Channels.[GuildEnvironment.channelWelcome] + 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 + try + let! _ = createInvite (ctx.GetDiscordMember().Id) invite.Code + return () + with ex -> + printfn "%A" ex.Message + () + + 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 { + do! removeInvitedUser eventArgs.Member.Id + } :> 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) + diff --git a/Bot/Items.json b/Bot/Items.json index 238bb0b..f4a7c1f 100644 --- a/Bot/Items.json +++ b/Bot/Items.json @@ -1,92 +1,205 @@ [ { - "Id": 0, - "Name": "Virus", - "Type": 0, - "Price": 0, - "Power": 25, - "Cooldown": 1, - "Class": 0, - "Attributes": { - "Sell": false, - "Buy": false, - "Consume": false, - "Drop": false - } + "Case": "Hack", + "Fields": [ + { + "Power": 25, + "Class": 0, + "Cooldown": 1, + "Item": { + "Id": 0, + "Name": "Virus", + "Price": 0 + } + } + ] }, { - "Id": 1, - "Name": "RemoteAccess", - "Type": 0, - "Price": 500, - "Power": 75, - "Cooldown": 3, - "Class": 1, - "Attributes": { - "Sell": true, - "Buy": true, - "Consume": false, - "Drop": true - } + "Case": "Hack", + "Fields": [ + { + "Power": 75, + "Class": 1, + "Cooldown": 3, + "Item": { + "Id": 1, + "Name": "Remote Access", + "Price": 500 + } + } + ] }, { - "Id": 2, - "Name": "Worm", - "Type": 0, - "Price": 5000, - "Power": 150, - "Cooldown": 5, - "Class": 2, - "Attributes": { - "Sell": true, - "Buy": true, - "Consume": false, - "Drop": true - } + "Case": "Hack", + "Fields": [ + { + "Power": 150, + "Class": 2, + "Cooldown": 5, + "Item": { + "Id": 2, + "Name": "Worm", + "Price": 5000 + } + } + ] }, { - "Id": 6, - "Name": "Firewall", - "Type": 1, - "Price": 0, - "Power": 10, - "Class": 0, - "Cooldown": 120, - "Attributes": { - "Sell": false, - "Buy": false, - "Consume": false, - "Drop": false - } + "Case": "Shield", + "Fields": [ + { + "Class": 0, + "Cooldown": 120, + "Item": { + "Id": 6, + "Name": "Firewall", + "Price": 0 + } + } + ] }, { - "Id": 7, - "Name": "Encryption", - "Type": 1, - "Price": 500, - "Power": 50, - "Class": 1, - "Cooldown": 240, - "Attributes": { - "Sell": true, - "Buy": true, - "Consume": false, - "Drop": true - } + "Case": "Shield", + "Fields": [ + { + "Class": 1, + "Cooldown": 240, + "Item": { + "Id": 7, + "Name": "Encryption", + "Price": 500 + } + } + ] }, { - "Id": 8, - "Name": "Cypher", - "Type": 1, - "Price": 5000, - "Power": 80, - "Class": 2, - "Cooldown": 380, - "Attributes": { - "Sell": true, - "Buy": true, - "Consume": false, - "Drop": true - } + "Case": "Shield", + "Fields": [ + { + "Class": 2, + "Cooldown": 360, + "Item": { + "Id": 8, + "Name": "Cypher", + "Price": 5000 + } + } + ] + }, + { + "Case": "Food", + "Fields": [ + { + "TargetStat" : 0, + "BoostAmount" : 30, + "Item": { + "Id": 12, + "Name": "Protein Powder", + "Price": 50 + } + } + ] + }, + { + "Case": "Food", + "Fields": [ + { + "TargetStat" : 1, + "BoostAmount" : 30, + "Item": { + "Id": 13, + "Name": "Toro Loco", + "Price": 50 + } + } + ] + }, + { + "Case": "Food", + "Fields": [ + { + "TargetStat" : 2, + "BoostAmount" : 30, + "Item": { + "Id": 14, + "Name": "Oldports Cigs", + "Price": 50 + } + } + ] + }, + { + "Case": "Food", + "Fields": [ + { + "TargetStat" : 3, + "BoostAmount" : 30, + "Item": { + "Id": 15, + "Name": "Moon Pie", + "Price": 50 + } + } + ] + }, + { + "Case": "Accessory", + "Fields": [ + { + "TargetStat" : 0, + "FloorBoost" : 25, + "CeilBoost" : 0, + "Item": { + "Id": 20, + "Name": "Kettlebell", + "Price": 250 + } + } + ] + }, + { + "Case": "Accessory", + "Fields": [ + { + "TargetStat" : 1, + "FloorBoost" : 25, + "CeilBoost" : 0, + "Item": { + "Id": 21, + "Name": "Headphones", + "Price": 250 + } + } + ] + }, + { + "Case": "Accessory", + "Fields": [ + { + "TargetStat" : 2, + "FloorBoost" : 0, + "CeilBoost" : 25, + "Item": { + "Id": 22, + "Name": "Rolox Watch", + "Price": 250 + } + } + ] + }, + { + "Case": "Accessory", + "Fields": [ + { + "TargetStat" : 3, + "FloorBoost" : 0, + "CeilBoost" : 25, + "Item": { + "Id": 23, + "Name": "Buddha Keychain", + "Price": 250 + } + } + ] } ] diff --git a/Bot/Messaging.fs b/Bot/Messaging.fs new file mode 100644 index 0000000..d8298f1 --- /dev/null +++ b/Bot/Messaging.fs @@ -0,0 +1,137 @@ +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 + } diff --git a/Bot/PlayerInteractions.fs b/Bot/PlayerInteractions.fs index 817006c..170c80d 100644 --- a/Bot/PlayerInteractions.fs +++ b/Bot/PlayerInteractions.fs @@ -1,75 +1,58 @@ module Degenz.PlayerInteractions open System.Threading.Tasks -open DSharpPlus.SlashCommands -open Degenz.Types +open DSharpPlus +open DSharpPlus.Entities +open Degenz.Messaging +open Degenz.DbService -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) +let executePlayerAction (ctx : IDiscordContext) (dispatch : PlayerData -> Async) = + async { + let builder = DiscordInteractionResponseBuilder().AsEphemeral(true) + do! ctx.Respond(InteractionResponseType.DeferredChannelMessageWithSource, builder) |> Async.AwaitTask + 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" + } |> Async.StartAsTask :> Task - { 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 - } - -// 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 () - - [] - member _.AddHackerRole (ctx : InteractionContext) = Commands.upsertPlayer ctx.Member.Id - -// [] -// member this.Leaderboard (ctx : InteractionContext) = Commands.leaderboard ctx +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 (ctx.GetDiscordMember().Id) + tryFindPlayer 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 (ctx.GetDiscordMember().Id) + tryFindPlayer 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 +let handleResultWithResponse ctx fn (player : Result) = + match player with + | Ok p -> fn p + | Error e -> async { do! Messaging.sendFollowUpMessage ctx e } 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/Store.fs b/Bot/Store.fs deleted file mode 100644 index 6a4a219..0000000 --- a/Bot/Store.fs +++ /dev/null @@ -1,120 +0,0 @@ -module Degenz.Store - -open System.Threading.Tasks -open DSharpPlus.Entities -open DSharpPlus -open DSharpPlus.EventArgs -open DSharpPlus.SlashCommands -open Degenz -open Degenz.Messaging - -let checkHasSufficientFunds (item : Item) player = - if player.Bank - item.Price >= 0 - then Ok 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) - 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) - then Ok player - else Error $"{item.Name} not found in your arsenal! Looks like you sold it already." - -let checkHasItemsInArsenal itemType player = - if player.Inventory |> Array.filter (fun i -> i.Type = itemType ) |> Array.length > 0 - then Ok player - else Error $"You currently have no {itemType}s in your arsenal to sell!" - -let buy itemType (ctx : IDiscordContext) = - Game.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 { - match checkHasItemsInArsenal itemType player with - | Ok _ -> let itemStore = Embeds.getSellEmbed itemType player - do! ctx.FollowUp(itemStore) |> Async.AwaitTask - | Error e -> do! sendFollowUpMessage ctx e - }) - -// TODO: When you buy a shield, prompt the user to activate it -let handleBuyItem (ctx : IDiscordContext) itemId = - Game.executePlayerAction ctx (fun player -> async { - let item = Armory.battleItems |> Array.find (fun w -> w.Id = 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 } - do! DbService.updatePlayer GuildEnvironment.pgDb p |> Async.Ignore - do! sendFollowUpMessage ctx $"Successfully purchased {item.Name}! You now have {newBalance} 💰$GBT remaining" - }) - }) - -let handleSell (ctx : IDiscordContext) itemId = - Game.executePlayerAction ctx (fun player -> async { - let item = Armory.getItem itemId - do! - player - |> checkSoldItemAlready item - |> handleResultWithResponse ctx (fun player -> async { - let updatedPlayer = { - player with - Bank = player.Bank + item.Price - Inventory = player.Inventory |> Array.filter (fun i -> i.Id <> itemId) - } - do! - [ DbService.updatePlayer GuildEnvironment.pgDb updatedPlayer |> Async.Ignore - DbService.removeShieldEvent GuildEnvironment.pgDb updatedPlayer.DiscordId itemId |> Async.Ignore - sendFollowUpMessage ctx $"Sold {item.Type} {item.Name} for {item.Price}! Current Balance: {updatedPlayer.Bank}" ] - |> Async.Parallel - |> Async.Ignore - - }) - }) - -let handleStoreEvents (_ : DiscordClient) (event : ComponentInteractionCreateEventArgs) = - let ctx = DiscordEventContext event :> IDiscordContext - let id = ctx.GetInteractionId() - let itemId = int <| id.Split("-").[1] - match id with - | id when id.StartsWith("Buy") -> handleBuyItem ctx itemId - | id when id.StartsWith("Sell") -> handleSell ctx itemId - | _ -> - task { - let builder = DiscordInteractionResponseBuilder() - builder.IsEphemeral <- true - builder.Content <- $"Incorrect Action identifier {id}" - do! ctx.Respond(InteractionResponseType.ChannelMessageWithSource, builder) |> Async.AwaitTask - } - -type Store() = - inherit ApplicationCommandModule () - - let enforceChannel (ctx : IDiscordContext) (storeFn : IDiscordContext -> Task) = - match ctx.GetChannel().Id with - | id when id = GuildEnvironment.channelArmory -> storeFn ctx - | _ -> - task { - let msg = $"You must go to <#{GuildEnvironment.channelArmory}> channel to buy or sell weapons" - do! Messaging.sendSimpleResponse ctx msg - } - - [] - member _.BuyHack (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (buy ItemType.Hack) - - [] - member this.BuyShield (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (buy ItemType.Shield) - - [] - member this.SellHack (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (sell ItemType.Hack) - - [] - member this.SellShield (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (sell ItemType.Shield) - 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 45a74ae..0000000 --- a/DbService/DbService.fsproj +++ /dev/null @@ -1,14 +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/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 deleted file mode 100644 index 21611f1..0000000 --- a/Shared/Shared.fs +++ /dev/null @@ -1,283 +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 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 } - 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