Merge branch 'staging'

This commit is contained in:
Joseph Ferano 2022-03-05 22:44:39 +07:00
commit e7bb53006f
27 changed files with 1496 additions and 1048 deletions

View File

@ -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<HackerGame>(guild);
storeCommands.RegisterCommands<Store>(guild);
stealCommands.RegisterCommands<StealGame>(guild);
hackerCommands.RegisterCommands<HackerBattle.HackerGame>(guild);
storeCommands.RegisterCommands<Store.Store>(guild);
stealCommands.RegisterCommands<Thief.StealGame>(guild);
inviterCommands.RegisterCommands<InviteTracker.Inviter>(guild);
//hackerCommands.RegisterCommands<RPSGame>(guild);
//sc3.RegisterCommands<SlotMachine>(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<PlayerInteractions.PlayerInteractions>(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)

View File

@ -11,21 +11,23 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="paket.references" />
<Compile Include="Prelude.fs" />
<Compile Include="GuildEnvironment.fs" />
<Compile Include="Game.fs" />
<Compile Include="XP.fs" />
<Compile Include="Messaging.fs" />
<Compile Include="GameTypes.fs" />
<Compile Include="GameHelpers.fs" />
<Compile Include="DbService.fs" />
<Compile Include="PlayerInteractions.fs" />
<Compile Include="InviteTracker.fs" />
<Compile Include="XP.fs" />
<Compile Include="Embeds.fs" />
<Compile Include="SlotMachine.fs" />
<Compile Include="Thief.fs" />
<Compile Include="RockPaperScissors.fs" />
<Compile Include="Store.fs" />
<Compile Include="Trainer.fs" />
<Compile Include="HackerBattle.fs" />
<Compile Include="Games\SlotMachine.fs" />
<Compile Include="Games\Thief.fs" />
<Compile Include="Games\RockPaperScissors.fs" />
<Compile Include="Games\Store.fs" />
<Compile Include="Games\Trainer.fs" />
<Compile Include="Games\HackerBattle.fs" />
<Compile Include="Bot.fs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DbService\DbService.fsproj" />
</ItemGroup>
<Import Project="..\.paket\Paket.Restore.targets" />
</Project>

View File

@ -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<GBT>
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,8 +44,25 @@ let getPlayerEvents connStr (player : PlayerData) =
)
|> Async.AwaitTask
let tryFindPlayer connStr (discordId : uint64) =
async {
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 tryFindPlayer (discordId : uint64) = async {
try
let! user =
// use cert = new X509Certificate2("~/Downloads/ca-certificate.crt")
@ -64,45 +75,63 @@ let tryFindPlayer connStr (discordId : uint64) =
|> 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
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<GBT>
Inventory = inv |> Array.toList
Strength = read.int "strength"
Inventory = read.intArray "inventory"
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 player = mapBack u
let! events = getPlayerEvents connStr player
return Some { player with Events = events |> List.toArray }
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 connStr (player : PlayerData) =
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 ->

View File

@ -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<ItemId>(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<ItemId>(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<DiscordComponent>
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<HackId>(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<ShieldId>(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<HackId>(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<ShieldId>(item.Id)))
|> ignore
embed
.AddField("Price 💰", (if item.Price = 0<GBT> 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<HackId>(item.Id))) |> ignore
| _ -> embed.WithThumbnail(getShieldIcon (enum<ShieldId>(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)

View File

@ -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<unit>) =
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<unit>) =
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<unit>) =
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<GBT> }
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}"

135
Bot/GameHelpers.fs Normal file
View File

@ -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<ItemDetails array>(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<GBT> }
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}"

180
Bot/GameTypes.fs Normal file
View File

@ -0,0 +1,180 @@
[<Microsoft.FSharp.Core.AutoOpen>]
module Degenz.Types
open System
open Degenz
[<Measure>]
type mins
[<Measure>]
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<mins>
Timestamp : DateTime }
[<RequireQualifiedAccess>]
type ItemType =
| Hack
| Shield
| Food
| Accessory
type Item = {
Id : int
Name : string
Price : int<GBT>
}
type HackItem = {
Power : int
Class : int
Cooldown : int<mins>
Item : Item
}
type ShieldItem = {
Class : int
Cooldown : int<mins>
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<GBT>
}
// 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<GBT> }

View File

@ -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<GBT> with
match target.Bank <= 0<GBT> 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<GBT> }
{ p with Events = attack::p.Events ; Bank = max (p.Bank + amount) 0<GBT> }
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<mins> else hack.Cooldown }
Cooldown = if isDefenderEvent then int WeaponClass.SameTargetAttackCooldown.TotalMinutes * 1<mins> 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<GBT>)
@ -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
})

View File

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

View File

@ -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() =
[<SlashCommand("spin", "Want to try your luck?")>]
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<GBT> }
do! DbService.updatePlayer { player with Bank = player.Bank + 10<GBT> }
|> Async.Ignore
else
do! DbService.updatePlayer GuildEnvironment.pgDb { player with Bank = max (player.Bank - 1<GBT>) 0<GBT> }
do! DbService.updatePlayer { player with Bank = max (player.Bank - 1<GBT>) 0<GBT> }
|> Async.Ignore

240
Bot/Games/Store.fs Normal file
View File

@ -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<GBT> 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<GBT>
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
}
// [<SlashCommand("buy-item", "Purchase an item")>]
// member _.BuyItem (ctx : InteractionContext) = checkChannel (DiscordInteractionContext(ctx))
//
[<SlashCommand("buy-hack", "Purchase a hack so you can take money from other Degenz")>]
member _.BuyHack (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (buy (Inventory.getItemsByType ItemType.Hack))
[<SlashCommand("buy-shield", "Purchase a hack shield so you can protect your GBT")>]
member this.BuyShield (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (buy (Inventory.getItemsByType ItemType.Shield))
// [<SlashCommand("buy-food", "Purchase a food item to help boost your stats")>]
// member this.BuyFood (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (buy (Inventory.getItemsByType ItemType.Food))
//
[<SlashCommand("sell-hack", "Sell a hack for GoodBoyTokenz")>]
member this.SellHack (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (sell "Hacks" (Inventory.getItemsByType ItemType.Hack))
[<SlashCommand("sell-shield", "Sell a shield for GoodBoyTokenz")>]
member this.SellShield (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (sell "Shields" (Inventory.getItemsByType ItemType.Shield))
[<SlashCommand("consume", "Consume a food item")>]
member this.Consume (ctx : InteractionContext) =
enforceChannel (DiscordInteractionContext(ctx)) (sell "Shields" (Inventory.getItemsByType ItemType.Food))
// [<SlashCommand("inventory", "Check your inventory")>]
// member this.Inventory (ctx : InteractionContext) =
// enforceChannel (DiscordInteractionContext(ctx)) (sell "Shields" (Inventory.getItemsByType ItemType))

View File

@ -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<GBT> with
match target.Bank <= 0<GBT> 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<GBT>
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<mins>
}
do! DbService.updatePlayer GuildEnvironment.pgDb { t with Bank = max (t.Bank - prize) 0<GBT> } |> Async.Ignore
do! DbService.addPlayerEvent GuildEnvironment.pgDb victim.DiscordId mugged |> Async.Ignore
do! DbService.updatePlayer { t with Bank = max (t.Bank - prize) 0<GBT> } |> 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<mins>
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<mins>
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

View File

@ -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<mins>
let HackEvent () = {
Timestamp = System.DateTime.UtcNow
Cooldown = 1<mins>
Type = Hacking {
Adversary = Sensei
Success = true
IsInstigator = true
HackId = defaultHack.Id } }
{ Timestamp = System.DateTime.UtcNow
HackId = defaultHack.Item.Id
}
}
let ShieldEvent () = {
Timestamp = System.DateTime.UtcNow
Cooldown = defaultShield.Cooldown
Type = Shielding defaultShield.Id }
|]
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<ShieldId>(int tokens.[2])
let shield = Armory.getItem (int shieldId)
let shieldId = enum<ItemId>(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 lets **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 {
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 = TrainerEvents |> Array.append player.Events
Inventory = [| defaultHack ; defaultShield |] |> Array.append player.Inventory
Events = shieldEvent @ player.Events
Inventory = hack @ shield @ 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
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

View File

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

210
Bot/InviteTracker.fs Normal file
View File

@ -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 ()
[<SlashCommand("invite-create", "Invite user to this discord and earn rewards")>]
member this.CreateInvite (ctx : InteractionContext) =
createGuildInvite (DiscordInteractionContext ctx)
[<SlashCommand("invites-list", "List all the invites")>]
member this.ListServerInvites (ctx : InteractionContext) =
listServerInvites (DiscordInteractionContext ctx)
[<SlashCommand("invites-attributions", "Get total invites from a specific user")>]
member this.getAttributions (ctx : InteractionContext, [<Option("player", "The player you want to check")>] user : DiscordUser) =
getAttributions (DiscordInteractionContext ctx) user.Id
[<SlashCommand("invited-people", "Get total invites from a specific user")>]
member this.ListInvitedPeople (ctx : InteractionContext, [<Option("player", "The player you want to check")>] user : DiscordUser) =
getInvitedUsersForId (DiscordInteractionContext ctx) user.Id
[<SlashCommand("invites-clear", "Get total invites from a specific user")>]
member this.ClearInvites (ctx : InteractionContext) =
clearInvites (DiscordInteractionContext ctx)

View File

@ -1,92 +1,205 @@
[
{
"Case": "Hack",
"Fields": [
{
"Power": 25,
"Class": 0,
"Cooldown": 1,
"Item": {
"Id": 0,
"Name": "Virus",
"Type": 0,
"Price": 0,
"Power": 25,
"Cooldown": 1,
"Class": 0,
"Attributes": {
"Sell": false,
"Buy": false,
"Consume": false,
"Drop": false
"Price": 0
}
}
]
},
{
"Id": 1,
"Name": "RemoteAccess",
"Type": 0,
"Price": 500,
"Case": "Hack",
"Fields": [
{
"Power": 75,
"Cooldown": 3,
"Class": 1,
"Attributes": {
"Sell": true,
"Buy": true,
"Consume": false,
"Drop": true
"Cooldown": 3,
"Item": {
"Id": 1,
"Name": "Remote Access",
"Price": 500
}
}
]
},
{
"Case": "Hack",
"Fields": [
{
"Power": 150,
"Class": 2,
"Cooldown": 5,
"Item": {
"Id": 2,
"Name": "Worm",
"Type": 0,
"Price": 5000,
"Power": 150,
"Cooldown": 5,
"Class": 2,
"Attributes": {
"Sell": true,
"Buy": true,
"Consume": false,
"Drop": true
"Price": 5000
}
}
]
},
{
"Id": 6,
"Name": "Firewall",
"Type": 1,
"Price": 0,
"Power": 10,
"Case": "Shield",
"Fields": [
{
"Class": 0,
"Cooldown": 120,
"Attributes": {
"Sell": false,
"Buy": false,
"Consume": false,
"Drop": false
"Item": {
"Id": 6,
"Name": "Firewall",
"Price": 0
}
}
]
},
{
"Id": 7,
"Name": "Encryption",
"Type": 1,
"Price": 500,
"Power": 50,
"Case": "Shield",
"Fields": [
{
"Class": 1,
"Cooldown": 240,
"Attributes": {
"Sell": true,
"Buy": true,
"Consume": false,
"Drop": true
"Item": {
"Id": 7,
"Name": "Encryption",
"Price": 500
}
}
]
},
{
"Case": "Shield",
"Fields": [
{
"Class": 2,
"Cooldown": 360,
"Item": {
"Id": 8,
"Name": "Cypher",
"Type": 1,
"Price": 5000,
"Power": 80,
"Class": 2,
"Cooldown": 380,
"Attributes": {
"Sell": true,
"Buy": true,
"Consume": false,
"Drop": true
"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
}
}
]
}
]

137
Bot/Messaging.fs Normal file
View File

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

View File

@ -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)
{ DiscordId = membr
Name = nickname
Inventory = [| hack ; shield |]
Events = [||]
// XP = 0
// Achievements = [||]
Traits = PlayerTraits.empty
Bank = 100<GBT> }
let upsertPlayer discordId =
let executePlayerAction (ctx : IDiscordContext) (dispatch : PlayerData -> Async<unit>) =
async {
let! player = DbService.tryFindPlayer GuildEnvironment.pgDb discordId
let! newPlayer =
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
let executePlayerActionWithTarget (targetPlayer : DiscordUser) (ctx : IDiscordContext) (dispatch : PlayerData -> PlayerData -> Async<unit>) =
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<unit>) =
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<PlayerData, string>) =
match player with
| Some _ -> async.Return false
| None ->
async {
// do! newPlayer "" discordId |> DbService.insertNewPlayer
return true
}
return newPlayer
}
[<CLIMutable>]
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 ()
[<SlashCommand("redpill", "Take the redpill and become a hacker")>]
member _.AddHackerRole (ctx : InteractionContext) = Commands.upsertPlayer ctx.Member.Id
// [<SlashCommand("leaderboard", "View the current list of players ranked by highest earnings")>]
// member this.Leaderboard (ctx : InteractionContext) = Commands.leaderboard ctx
| Ok p -> fn p
| Error e -> async { do! Messaging.sendFollowUpMessage ctx e }

22
Bot/Prelude.fs Normal file
View File

@ -0,0 +1,22 @@
namespace Degenz
[<Microsoft.FSharp.Core.AutoOpen>]
module ResultHelpers =
let (>>=) x f = Result.bind f x
let (<!>) x f = Result.map f x
[<Microsoft.FSharp.Core.AutoOpen>]
[<RequireQualifiedAccess>]
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

View File

@ -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<GBT>
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
}
[<SlashCommand("buy-hack", "Purchase a hack attack you can use to earn GoodBoyTokenz")>]
member _.BuyHack (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (buy ItemType.Hack)
[<SlashCommand("buy-shield", "Purchase a hack shield so you can protect your GoodBoyTokenz")>]
member this.BuyShield (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (buy ItemType.Shield)
[<SlashCommand("sell-hack", "Sell a hack for GoodBoyTokenz")>]
member this.SellHack (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (sell ItemType.Hack)
[<SlashCommand("sell-shield", "Sell a shield for GoodBoyTokenz")>]
member this.SellShield (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (sell ItemType.Shield)

View File

@ -6,7 +6,7 @@ open DSharpPlus.Entities
type RewardType =
| Currency of int<GBT>
| RandomItem of itemType : ItemType * amount : int
// | RandomItem of itemType : ItemType * amount : int
| SpecialItem of id : int
[<Literal>]

View File

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<Compile Include="DbService.fs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Shared\Shared.fsproj" />
</ItemGroup>
<Import Project="..\.paket\Paket.Restore.targets" />
</Project>

View File

@ -1,8 +0,0 @@
FSharp.Core
DSharpPlus
// DSharpPlus.CommandsNext
// DSharpPlus.Interactivity
DSharpPlus.SlashCommands
MongoDB.Driver
Npgsql.FSharp

View File

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

View File

@ -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
[<Microsoft.FSharp.Core.AutoOpen>]
module ResultHelpers =
let (>>=) x f = Result.bind f x
let (<!>) x f = Result.map f x
[<RequireQualifiedAccess>]
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
[<Microsoft.FSharp.Core.AutoOpen>]
module Types =
[<Measure>]
type mins
[<Measure>]
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<GBT>
Type : ItemType
Power : int
Class : int
Cooldown : int<mins>
Attributes : ItemAttributes
}
with static member empty =
{ Id = -1
Name = "None"
Price = 0<GBT>
Type = ItemType.Hack
Power = 0
Class = -1
Cooldown = 0<mins>
Attributes = ItemAttributes.empty }
type HackResult =
| Strong
| Weak
[<CLIMutable>]
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<mins>
Timestamp : DateTime }
type PlayerTraits = {
Strength : int
Focus : int
Luck : int
Charisma : int
}
with static member empty = { Strength = 0 ; Focus = 0 ; Luck = 0 ; Charisma = 0 }
[<CLIMutable>]
type PlayerData = {
DiscordId : uint64
Name : string
Inventory : Item array
Events : PlayerEvent array
Traits : PlayerTraits
Bank : int<GBT>
}
// 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<GBT> }
module Armory =
let battleItems =
let file = System.IO.File.ReadAllText("Items.json")
// let file = System.IO.File.ReadAllText("Bot/Items.json")
JsonConvert.DeserializeObject<Item array>(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<PlayerData, string>) =
match player with
| Ok p -> fn p
| Error e -> async { do! sendFollowUpMessage ctx e }

View File

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<Content Include="paket.references" />
<Compile Include="Shared.fs" />
</ItemGroup>
<Import Project="..\.paket\Paket.Restore.targets" />
</Project>

View File

@ -1,3 +0,0 @@
FSharp.Core
DSharpPlus
DSharpPlus.SlashCommands