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 module Degenz.Bot
open System.IO
open System.IO.Pipes
open System.Threading.Tasks open System.Threading.Tasks
open DSharpPlus open DSharpPlus
open DSharpPlus.SlashCommands open DSharpPlus.SlashCommands
open Degenz open Degenz
open Degenz.HackerBattle
open Degenz.Store
open Degenz.Thief
open Emzi0767.Utilities open Emzi0767.Utilities
//open Degenz.SlotMachine //open Degenz.SlotMachine
@ -17,11 +16,10 @@ let guild = GuildEnvironment.guildId
let hackerBattleConfig = DiscordConfiguration() let hackerBattleConfig = DiscordConfiguration()
let storeConfig = DiscordConfiguration() let storeConfig = DiscordConfiguration()
let stealConfig = DiscordConfiguration() let stealConfig = DiscordConfiguration()
let inviterConfig = DiscordConfiguration()
//let slotMachineConfig = DiscordConfiguration() //let slotMachineConfig = DiscordConfiguration()
//hackerBattleConfig.MinimumLogLevel <- Microsoft.Extensions.Logging.LogLevel.Trace //hackerBattleConfig.MinimumLogLevel <- Microsoft.Extensions.Logging.LogLevel.Trace
//storeConfig.MinimumLogLevel <- Microsoft.Extensions.Logging.LogLevel.Trace
//let configs = [| hackerBattleConfig ; storeConfig ; slotMachineConfig ; |]
let configs = [ hackerBattleConfig ; storeConfig ; stealConfig ]
hackerBattleConfig.TokenType <- TokenType.Bot hackerBattleConfig.TokenType <- TokenType.Bot
hackerBattleConfig.Intents <- DiscordIntents.All hackerBattleConfig.Intents <- DiscordIntents.All
@ -32,31 +30,40 @@ storeConfig.Intents <- DiscordIntents.All
stealConfig.TokenType <- TokenType.Bot stealConfig.TokenType <- TokenType.Bot
stealConfig.Intents <- DiscordIntents.All stealConfig.Intents <- DiscordIntents.All
inviterConfig.TokenType <- TokenType.Bot
inviterConfig.Intents <- DiscordIntents.All
hackerBattleConfig.Token <- GuildEnvironment.tokenHackerBattle hackerBattleConfig.Token <- GuildEnvironment.tokenHackerBattle
storeConfig.Token <- GuildEnvironment.tokenStore storeConfig.Token <- GuildEnvironment.tokenStore
stealConfig.Token <- GuildEnvironment.tokenSteal stealConfig.Token <- GuildEnvironment.tokenSteal
inviterConfig.Token <- GuildEnvironment.tokenInviter
//slotMachineConfig.Token <- Environment.GetEnvironmentVariable("BOT_SLOT_MACHINE") //slotMachineConfig.Token <- Environment.GetEnvironmentVariable("BOT_SLOT_MACHINE")
let hackerBattleBot = new DiscordClient(hackerBattleConfig) let hackerBattleBot = new DiscordClient(hackerBattleConfig)
let storeBot = new DiscordClient(storeConfig) let storeBot = new DiscordClient(storeConfig)
let stealBot = new DiscordClient(stealConfig) let stealBot = new DiscordClient(stealConfig)
let inviterBot = new DiscordClient(inviterConfig)
//let slotMachineBot = new DiscordClient(slotMachineConfig) //let slotMachineBot = new DiscordClient(slotMachineConfig)
//let clients = [| hackerBattleBot ; storeBot ; slotMachineBot |] //let clients = [| hackerBattleBot ; storeBot ; slotMachineBot |]
let hackerCommands = hackerBattleBot.UseSlashCommands() let hackerCommands = hackerBattleBot.UseSlashCommands()
let storeCommands = storeBot.UseSlashCommands() let storeCommands = storeBot.UseSlashCommands()
let stealCommands = stealBot.UseSlashCommands() let stealCommands = stealBot.UseSlashCommands()
let inviterCommands = inviterBot.UseSlashCommands()
//let sc3 = slotMachineBot.UseSlashCommands() //let sc3 = slotMachineBot.UseSlashCommands()
hackerCommands.RegisterCommands<HackerGame>(guild); hackerCommands.RegisterCommands<HackerBattle.HackerGame>(guild);
storeCommands.RegisterCommands<Store>(guild); storeCommands.RegisterCommands<Store.Store>(guild);
stealCommands.RegisterCommands<StealGame>(guild); stealCommands.RegisterCommands<Thief.StealGame>(guild);
inviterCommands.RegisterCommands<InviteTracker.Inviter>(guild);
//hackerCommands.RegisterCommands<RPSGame>(guild); //hackerCommands.RegisterCommands<RPSGame>(guild);
//sc3.RegisterCommands<SlotMachine>(guild); //sc3.RegisterCommands<SlotMachine>(guild);
hackerBattleBot.add_ComponentInteractionCreated(AsyncEventHandler(HackerBattle.handleButtonEvent)) hackerBattleBot.add_ComponentInteractionCreated(AsyncEventHandler(HackerBattle.handleButtonEvent))
storeBot.add_ComponentInteractionCreated(AsyncEventHandler(Store.handleStoreEvents)) storeBot.add_ComponentInteractionCreated(AsyncEventHandler(Store.handleStoreEvents))
stealBot.add_ComponentInteractionCreated(AsyncEventHandler(Thief.handleStealButton)) 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) = let asdf (_ : DiscordClient) (event : DSharpPlus.EventArgs.InteractionCreateEventArgs) =
async { async {
@ -74,8 +81,8 @@ let asdf (_ : DiscordClient) (event : DSharpPlus.EventArgs.InteractionCreateEven
:> Task :> Task
//hackerBattleBot.add_InteractionCreated(AsyncEventHandler(asdf)) //hackerBattleBot.add_InteractionCreated(AsyncEventHandler(asdf))
if guild <> 922419263275425832uL then //if guild <> 922419263275425832uL then
Trainer.sendInitialEmbed hackerBattleBot // Trainer.sendInitialEmbed hackerBattleBot
hackerBattleBot.ConnectAsync() |> Async.AwaitTask |> Async.RunSynchronously hackerBattleBot.ConnectAsync() |> Async.AwaitTask |> Async.RunSynchronously
GuildEnvironment.botUserHackerBattle <- Some hackerBattleBot.CurrentUser GuildEnvironment.botUserHackerBattle <- Some hackerBattleBot.CurrentUser
@ -85,32 +92,38 @@ GuildEnvironment.botUserArmory <- Some storeBot.CurrentUser
//stealBot.ConnectAsync() |> Async.AwaitTask |> Async.RunSynchronously //stealBot.ConnectAsync() |> Async.AwaitTask |> Async.RunSynchronously
//async { //inviterBot.ConnectAsync() |> Async.AwaitTask |> Async.RunSynchronously
// let! user = hackerBattleBot.GetUserAsync(GuildEnvironment.botIdHackerBattle) |> Async.AwaitTask
// if user <> null then
// GuildEnvironment.botUserHackerBattle <- Some user
// return ()
//} |> 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 rec loop areBotsRunning =
let interactionsConfig = DiscordConfiguration() async {
interactionsConfig.TokenType <- TokenType.Bot if not (File.Exists "fsharp-bots") then
interactionsConfig.Intents <- DiscordIntents.All use file = File.Create "fsharp-bots"
interactionsConfig.Token <- GuildEnvironment.tokenPlayerInteractions file.Flush()
let interactionsBot = new DiscordClient(interactionsConfig) let! file = File.ReadAllTextAsync("fsharp-bots") |> Async.AwaitTask
let commands = interactionsBot.UseSlashCommands() let! ran =
commands.RegisterCommands<PlayerInteractions.PlayerInteractions>(guild) 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) Task.Delay(-1)

View File

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

View File

@ -1,32 +1,26 @@
module Degenz.DbService module Degenz.DbService
open System.Security.Cryptography.X509Certificates
open Degenz.Types
open System open System
open Npgsql.FSharp open Npgsql.FSharp
open Degenz
let connStr = GuildEnvironment.connectionString
type User = { type User = {
Name : string Name : string
DiscordId : uint64 DiscordId : uint64
Bank : int<GBT> Bank : int<GBT>
Inventory : int list
Strength : int Strength : int
Inventory : int array Focus : int
Charisma : int
Luck : int
} }
let mapBack user : PlayerData = let getPlayerEvents (did : uint64) =
{ 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) =
connStr connStr
|> Sql.connect |> Sql.connect
|> Sql.parameters [ "did", Sql.string (string player.DiscordId) ] |> Sql.parameters [ "did", Sql.string (string did) ]
|> Sql.query """ |> Sql.query """
WITH usr AS (SELECT id FROM "user" WHERE discord_id = @did) 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 SELECT event_type, success, is_instigator, item_id, cooldown, adversary_id, adversary_name, created_at
@ -50,59 +44,94 @@ let getPlayerEvents connStr (player : PlayerData) =
) )
|> Async.AwaitTask |> Async.AwaitTask
let tryFindPlayer connStr (discordId : uint64) = let updatePlayerStats (player : PlayerData) =
async { connStr
try |> Sql.connect
let! user = |> Sql.parameters
// use cert = new X509Certificate2("~/Downloads/ca-certificate.crt") [ ( "did" , Sql.string (string player.DiscordId) )
// (Uri connStr) ( "strength", Sql.int player.Stats.Strength.Amount )
// |> Sql.fromUriToConfig ( "focus", Sql.int player.Stats.Focus.Amount )
// |> Sql.requireSslMode ( "charisma", Sql.int player.Stats.Charisma.Amount )
// |> Sql.formatConnectionString ( "luck", Sql.int player.Stats.Luck.Amount ) ]
// |> Sql.clientCertificate cert |> Sql.query """
connStr WITH usr AS (SELECT id FROM "user" WHERE discord_id = @did)
|> Sql.connect UPDATE player_stat SET strength = @strength, focus = @focus, charisma = @charisma, luck = @luck,
|> Sql.parameters [ "did", Sql.string (string discordId) ] updated_at = now() at time zone 'utc'
|> Sql.query """ FROM usr WHERE usr.id = user_id;
SELECT discord_id, display_name, gbt, strength, inventory FROM "user" WHERE discord_id = @did """
""" |> Sql.executeNonQueryAsync
|> Sql.executeAsync (fun read -> |> Async.AwaitTask
{
DiscordId = read.string "discord_id" |> uint64
Name = read.string "display_name"
Bank = read.int "gbt" * 1<GBT>
Strength = read.int "strength"
Inventory = read.intArray "inventory"
})
|> Async.AwaitTask
match List.tryHead user with
| None -> return None
| Some u ->
let player = mapBack u
let! events = getPlayerEvents connStr player
return Some { player with Events = events |> List.toArray }
with e ->
printfn $"Got an error{e.Message}"
return None
}
let updatePlayer connStr (player : PlayerData) = let tryFindPlayer (discordId : uint64) = async {
try
let! user =
// use cert = new X509Certificate2("~/Downloads/ca-certificate.crt")
// (Uri connStr)
// |> Sql.fromUriToConfig
// |> Sql.requireSslMode
// |> Sql.formatConnectionString
// |> Sql.clientCertificate cert
connStr
|> Sql.connect
|> Sql.parameters [ "did", Sql.string (string discordId) ]
|> Sql.query """
SELECT discord_id, display_name, gbt, inventory, strength, focus, charisma, luck FROM "user"
WHERE discord_id = @did
"""
|> Sql.executeAsync (fun read ->
let inv = read.intArray "inventory"
{
DiscordId = read.string "discord_id" |> uint64
Name = read.string "display_name"
Bank = read.int "gbt" * 1<GBT>
Inventory = inv |> Array.toList
Strength = read.int "strength"
Focus = read.int "focus"
Charisma = read.int "charisma"
Luck = read.int "luck"
})
|> Async.AwaitTask
match List.tryHead user with
| None -> return None
| Some u ->
let! events = getPlayerEvents u.DiscordId
let inventory = u.Inventory |> List.choose (fun id -> Armory.weapons |> List.tryFind (fun item -> item.Id = id))
let strength = PlayerStats.calculateActiveStat StatId.Strength u.Strength inventory
let focus = PlayerStats.calculateActiveStat StatId.Focus u.Focus inventory
let charisma = PlayerStats.calculateActiveStat StatId.Charisma u.Charisma inventory
let luck = PlayerStats.calculateActiveStat StatId.Luck u.Luck inventory
return Some
{ DiscordId = u.DiscordId
Name = u.Name
Inventory = inventory
Events = events
Stats = { Strength = strength ; Focus = focus ; Charisma = charisma ; Luck = luck }
Bank = u.Bank }
with e ->
printfn $"Got an error{e.Message}"
return None
}
let updatePlayer (player : PlayerData) =
connStr connStr
|> Sql.connect |> Sql.connect
|> Sql.parameters [ |> Sql.parameters [
"did", Sql.string (string player.DiscordId) "did", Sql.string (string player.DiscordId)
"gbt", Sql.int (int player.Bank) "gbt", Sql.int (int player.Bank)
"str", Sql.int (int player.Traits.Strength) "inv", Sql.intArray (player.Inventory |> Array.ofList |> Array.map (fun item -> item.Id))
"inv", Sql.intArray (player.Inventory |> Array.map (fun i -> i.Id)) "strength", Sql.int player.Stats.Strength.Amount
] "focus", Sql.int player.Stats.Focus.Amount
|> Sql.query """ "charisma", Sql.int player.Stats.Charisma.Amount
UPDATE "user" SET gbt = @gbt, strength = @str, inventory = @inv "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 WHERE discord_id = @did
""" """
|> Sql.executeNonQueryAsync |> Sql.executeNonQueryAsync
|> Async.AwaitTask |> Async.AwaitTask
let addAchievement connStr (did : uint64) (achievement : string) = let addAchievement (did : uint64) (achievement : string) =
connStr connStr
|> Sql.connect |> Sql.connect
|> Sql.parameters |> Sql.parameters
@ -116,7 +145,7 @@ let addAchievement connStr (did : uint64) (achievement : string) =
|> Sql.executeNonQueryAsync |> Sql.executeNonQueryAsync
|> Async.AwaitTask |> Async.AwaitTask
let checkHasAchievement connStr (did : uint64) (achievement : string) = async { let checkHasAchievement (did : uint64) (achievement : string) = async {
let! result = let! result =
connStr connStr
|> Sql.connect |> Sql.connect
@ -133,7 +162,7 @@ let checkHasAchievement connStr (did : uint64) (achievement : string) = async {
return List.isEmpty result |> not return List.isEmpty result |> not
} }
let removeShieldEvent connStr (did : uint64) shieldId = let removeShieldEvent (did : uint64) shieldId =
connStr connStr
|> Sql.connect |> Sql.connect
|> Sql.parameters |> Sql.parameters
@ -146,7 +175,7 @@ let removeShieldEvent connStr (did : uint64) shieldId =
|> Sql.executeNonQueryAsync |> Sql.executeNonQueryAsync
|> Async.AwaitTask |> Async.AwaitTask
let addPlayerEvent connStr (did : uint64) (playerEvent : PlayerEvent) = let addPlayerEvent (did : uint64) (playerEvent : PlayerEvent) =
let sqlParams , query = let sqlParams , query =
match playerEvent.Type with match playerEvent.Type with
| Hacking h -> | Hacking h ->

View File

@ -1,7 +1,6 @@
module Degenz.Embeds module Degenz.Embeds
open System open System
open DSharpPlus
open Degenz.Messaging open Degenz.Messaging
open Degenz.Types open Degenz.Types
open DSharpPlus.Entities open DSharpPlus.Entities
@ -9,43 +8,37 @@ open DSharpPlus.Entities
let hackGif = "https://s10.gifyu.com/images/Hacker-Degenz-V20ce8eb832734aa62-min.gif" 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 shieldGif = "https://s10.gifyu.com/images/Defense-Degenz-V2-min.gif"
let getHackIcon = function let getItemIcon id =
| HackId.Virus -> "https://s10.gifyu.com/images/Virus-icon.jpg" match enum<ItemId>(id) with
| HackId.RemoteAccess -> "https://s10.gifyu.com/images/Mind-Control-Degenz-V2-min.jpg" | ItemId.Virus -> "https://s10.gifyu.com/images/Virus-icon.jpg"
| HackId.Worm -> "https://s10.gifyu.com/images/WormBugAttack_Degenz-min.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 | _ -> hackGif
let getShieldIcon = function let getItemGif id =
| ShieldId.Firewall -> "https://s10.gifyu.com/images/Defense-GIF-1-Degenz-1.jpg" match enum<ItemId>(id) with
| ShieldId.Encryption -> "https://s10.gifyu.com/images/Encryption-Degenz-V2-1-min.jpg" | ItemId.Virus -> "https://s10.gifyu.com/images/Attack-DegenZ-1.gif"
| ShieldId.Cypher -> "https://s10.gifyu.com/images/Cypher-Smaller.jpg" | ItemId.RemoteAccess -> "https://s10.gifyu.com/images/Mind-Control-Degenz-V2-min.gif"
| _ -> shieldGif | ItemId.Worm -> "https://s10.gifyu.com/images/WormBugAttack_Degenz-min.gif"
| ItemId.Firewall -> "https://s10.gifyu.com/images/Defense-GIF-1-Degenz-min.gif"
let getHackGif = function | ItemId.Encryption -> "https://s10.gifyu.com/images/Encryption-Degenz-V2-1-min.gif"
| HackId.Virus -> "https://s10.gifyu.com/images/Attack-DegenZ-1.gif" | ItemId.Cypher -> "https://s10.gifyu.com/images/Cypher-Smaller.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"
| _ -> hackGif | _ -> hackGif
let getShieldGif = function let constructButtons (actionId: string) (buttonInfo : string) (player: PlayerData) (items : Inventory) ignoreCooldown =
| ShieldId.Firewall -> "https://s10.gifyu.com/images/Defense-GIF-1-Degenz-min.gif" items
| ShieldId.Encryption -> "https://s10.gifyu.com/images/Encryption-Degenz-V2-1-min.gif" |> List.map (fun item ->
| 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 action = let action =
player.Events player.Events
|> Array.tryFind (fun i -> |> List.tryFind (fun event ->
match i.Type with match event.Type with
| Hacking h -> h.HackId = item.Id && h.IsInstigator | Hacking h -> h.HackId = item.Id && h.IsInstigator
| Shielding id -> id = item.Id | Shielding id -> id = item.Id
| _ -> false) | _ -> false)
let btnColor = Game.getClassButtonColor item.Class let btnColor = WeaponClass.getClassButtonColor item
match action , ignoreCooldown with match action , ignoreCooldown with
| None , _ | Some _ , true -> | None , _ | Some _ , true ->
DiscordButtonComponent(btnColor, $"{actionId}-{item.Id}-{buttonInfo}-{player.Name}", $"{item.Name}") 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> |> Seq.cast<DiscordComponent>
let pickDefense actionId player isTrainer = 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 = let embed =
DiscordEmbedBuilder() DiscordEmbedBuilder()
.WithTitle("Shield Defense") .WithTitle("Shield Defense")
.WithDescription("Pick a shield to protect yourself from hacks") .WithDescription("Pick a shield to protect yourself from hacks")
for s in Player.getShields player |> Array.sortBy (fun i -> i.Power) do for shield in Inventory.getShields player.Inventory do
let hours = TimeSpan.FromMinutes(int s.Cooldown).TotalHours let hours = TimeSpan.FromMinutes(int shield.Cooldown).TotalHours |> int
let against = Game.getGoodAgainst(s.Class) |> snd let against = WeaponClass.getGoodAgainst(shield.Class) |> snd
embed.AddField(s.Name, $"Active {hours} hours\nDefeats {against}", true) |> ignore embed.AddField(shield.Item.Name, $"Active {hours} hours\nDefeats {against}", true) |> ignore
DiscordFollowupMessageBuilder() DiscordFollowupMessageBuilder()
.AddComponents(buttons) .AddComponents(buttons)
@ -73,7 +67,8 @@ let pickDefense actionId player isTrainer =
.AsEphemeral(true) .AsEphemeral(true)
let pickHack actionId attacker defender isTrainer = 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 stealMsg = if not isTrainer then $"{defender.Name} has **{defender.Bank} $GBT** we can take from them. " else ""
let embed = let embed =
@ -82,31 +77,31 @@ let pickHack actionId attacker defender isTrainer =
.WithDescription($"{stealMsg}Pick the hack you want to use.") .WithDescription($"{stealMsg}Pick the hack you want to use.")
if not isTrainer then if not isTrainer then
for h in Player.getHacks attacker |> Array.sortBy (fun i -> i.Power) do for hack in Inventory.getHacks attacker.Inventory do
let amount = if h.Power > int defender.Bank then int defender.Bank else h.Power let amount = if hack.Power > int defender.Bank then int defender.Bank else hack.Power
embed.AddField(h.Name, $"Cooldown {h.Cooldown} mins\nExtract {amount} $GBT", true) |> ignore embed.AddField(hack.Item.Name, $"Cooldown {hack.Cooldown} mins\nExtract {amount} $GBT", true) |> ignore
DiscordFollowupMessageBuilder() DiscordFollowupMessageBuilder()
.AddComponents(buttons) .AddComponents(buttons)
.AddEmbeds([ DiscordEmbedBuilder().WithImageUrl(hackGif).Build() ; embed.Build() ]) .AddEmbeds([ DiscordEmbedBuilder().WithImageUrl(hackGif).Build() ; embed.Build() ])
.AsEphemeral true .AsEphemeral true
let responseSuccessfulHack earnedMoney (targetId : uint64) amountTaken (hack : Item) = let responseSuccessfulHack earnedMoney (targetId : uint64) amountTaken (hack : HackItem) =
let embed = let embed =
DiscordEmbedBuilder() DiscordEmbedBuilder()
.WithImageUrl(getHackGif (enum<HackId>(hack.Id))) .WithImageUrl(getItemGif hack.Item.Id)
.WithTitle("Hack Attack") .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 "!")) + (if earnedMoney then $", and took {amountTaken} 💰$GBT from them!" else "!"))
DiscordFollowupMessageBuilder() DiscordFollowupMessageBuilder()
.AddEmbed(embed.Build()) .AddEmbed(embed.Build())
.AsEphemeral(true) .AsEphemeral(true)
let responseCreatedShield (shield : Item) = let responseCreatedShield (shield : ShieldItem) =
let embed = DiscordEmbedBuilder().WithImageUrl(getShieldGif (enum<ShieldId>(shield.Id))) let embed = DiscordEmbedBuilder().WithImageUrl(getItemGif shield.Item.Id)
embed.Title <- "Mounted Shield" 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() DiscordFollowupMessageBuilder()
.AddEmbed(embed) .AddEmbed(embed)
@ -116,66 +111,6 @@ let eventSuccessfulHack (ctx : IDiscordContext) target prize =
DiscordMessageBuilder() DiscordMessageBuilder()
.WithContent($"{ctx.GetDiscordMember().Username} successfully hacked <@{target.DiscordId}> and took {prize} GoodBoyTokenz") .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) = let getArsenalEmbed (player : PlayerData) =
DiscordFollowupMessageBuilder() DiscordFollowupMessageBuilder()
.AsEphemeral(true) .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 module Degenz.HackerBattle
open System open System
open System.Text
open System.Threading.Tasks open System.Threading.Tasks
open DSharpPlus open DSharpPlus
open DSharpPlus.Entities open DSharpPlus.Entities
@ -8,6 +9,7 @@ open DSharpPlus.EventArgs
open DSharpPlus.SlashCommands open DSharpPlus.SlashCommands
open Degenz open Degenz
open Degenz.Messaging open Degenz.Messaging
open Degenz.PlayerInteractions
let checkPlayerIsAttackingThemselves defender attacker = let checkPlayerIsAttackingThemselves defender attacker =
match attacker.DiscordId = defender.DiscordId with match attacker.DiscordId = defender.DiscordId with
@ -18,7 +20,7 @@ let checkAlreadyHackedTarget defender attacker =
defender defender
|> Player.removeExpiredActions |> Player.removeExpiredActions
|> fun d -> d.Events |> fun d -> d.Events
|> Array.tryFind (fun event -> |> List.tryFind (fun event ->
match event.Type with match event.Type with
| Hacking h -> h.Adversary.Id = attacker.DiscordId && h.IsInstigator = false | Hacking h -> h.Adversary.Id = attacker.DiscordId && h.IsInstigator = false
| _ -> false) | _ -> false)
@ -31,7 +33,7 @@ let checkAlreadyHackedTarget defender attacker =
let checkWeaponHasCooldown (weapon : Item) attacker = let checkWeaponHasCooldown (weapon : Item) attacker =
attacker.Events attacker.Events
|> Array.tryFind (fun a -> |> List.tryFind (fun a ->
match a.Type with match a.Type with
| Hacking h -> h.HackId = weapon.Id && h.IsInstigator | Hacking h -> h.HackId = weapon.Id && h.IsInstigator
| Shielding id -> id = weapon.Id | Shielding id -> id = weapon.Id
@ -43,68 +45,63 @@ let checkWeaponHasCooldown (weapon : Item) attacker =
| None -> Ok attacker | None -> Ok attacker
let checkHasEmptyHacks attacker = let checkHasEmptyHacks attacker =
match Player.getHacks attacker with match Inventory.getHacks attacker.Inventory with
| [||] -> Error $"You currently do not have any Hacks to take 💰$GBT from others. Please go to the <#{GuildEnvironment.channelArmory}> and purchase one." | [] -> Error $"You currently do not have any Hacks to take 💰$GBT from others. Please go to the <#{GuildEnvironment.channelArmory}> and purchase one."
| _ -> Ok attacker | _ -> Ok attacker
let checkPlayerOwnsWeapon (item : Item) player = 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 | true -> Ok player
| false -> Error $"You sold your weapon already, you cheeky bastard..." | false -> Error $"You sold your weapon already, you cheeky bastard..."
let checkPlayerHasShieldSlotsAvailable (shield : Item) player = let checkPlayerHasShieldSlotsAvailable player =
let updatedPlayer = player |> Player.removeExpiredActions let updatedPlayer = player |> Player.removeExpiredActions
let defenses = Player.getShieldEvents updatedPlayer let defenses = Player.getShieldEvents updatedPlayer
match defenses |> Array.length >= 3 with match defenses |> List.length >= 3 with
| true -> | true ->
let timestamp = defenses |> Array.rev |> Array.head |> fun a -> a.Timestamp // This should be the next expiring timestamp let event = defenses |> List.rev |> List.head // This should be the next expiring timestamp
let cooldown = getTimeText true (TimeSpan.FromMinutes(int shield.Cooldown)) 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" Error $"You are only allowed three shields at a time. Wait {cooldown} to add another shield"
| false -> Ok updatedPlayer | false -> Ok updatedPlayer
let checkTargetHasFunds target player = 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." | true -> Error $"Looks like the poor bastard has no $GBT... pick a different victim."
| false -> Ok player | false -> Ok player
let calculateDamage (hack : Item) (shield : Item) = let runHackerBattle defender (hack : HackItem) =
if hack.Class = shield.Class
then Weak
else Strong
let runHackerBattle defender hack =
defender defender
|> Player.removeExpiredActions |> Player.removeExpiredActions
|> fun p -> p.Events |> fun p -> p.Events
|> Array.choose (fun event -> |> List.choose (fun event ->
match event.Type with 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) | _ -> None)
|> Array.map (calculateDamage hack) |> List.map (fun shield -> if hack.Class = shield.Class then Weak else Strong)
|> Array.contains Weak |> 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 = 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 event isDefenderEvent =
let hackEvent = { let hackEvent = {
HackId = hack.Id HackId = hack.Item.Id
Adversary = if isDefenderEvent then attacker.basicPlayer else defender.basicPlayer Adversary = if isDefenderEvent then attacker.toDiscordPlayer else defender.toDiscordPlayer
IsInstigator = not isDefenderEvent IsInstigator = not isDefenderEvent
Success = successfulHack Success = successfulHack
} }
{ Type = Hacking hackEvent { Type = Hacking hackEvent
Timestamp = DateTime.UtcNow 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 <| updatePlayer prize (event false) attacker
DbService.updatePlayer GuildEnvironment.pgDb <| updatePlayer -prize (event true) defender DbService.updatePlayer <| updatePlayer -prize (event true) defender
DbService.addPlayerEvent GuildEnvironment.pgDb attacker.DiscordId (event false) DbService.addPlayerEvent attacker.DiscordId (event false)
DbService.addPlayerEvent GuildEnvironment.pgDb defender.DiscordId (event true) ] DbService.addPlayerEvent defender.DiscordId (event true) ]
|> Async.Parallel |> Async.Parallel
|> Async.Ignore |> Async.Ignore
let successfulHack (ctx : IDiscordContext) attacker defender hack = let successfulHack (ctx : IDiscordContext) attacker defender (hack : HackItem) =
async { async {
let prizeAmount = if hack.Power < int defender.Bank then hack.Power else int defender.Bank let prizeAmount = if hack.Power < int defender.Bank then hack.Power else int defender.Bank
do! updateCombatants true attacker defender hack (prizeAmount * 1<GBT>) do! updateCombatants true attacker defender hack (prizeAmount * 1<GBT>)
@ -119,7 +116,7 @@ let successfulHack (ctx : IDiscordContext) attacker defender hack =
|> Async.Ignore |> Async.Ignore
} }
let failedHack (ctx : IDiscordContext) attacker defender hack = let failedHack (ctx : IDiscordContext) attacker defender (hack : HackItem) =
async { async {
let lostAmount = if hack.Power < int attacker.Bank then hack.Power else int attacker.Bank let lostAmount = if hack.Power < int attacker.Bank then hack.Power else int attacker.Bank
let msg = $"Hack failed! {defender.Name} was able to mount a successful defense! You lost {lostAmount} $GBT!" 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) = let hack (target : DiscordUser) (ctx : IDiscordContext) =
Game.executePlayerActionWithTarget target ctx (fun attacker defender -> async { executePlayerActionWithTarget target ctx (fun attacker defender -> async {
do! attacker do! attacker
|> Player.removeExpiredActions |> Player.removeExpiredActions
|> checkAlreadyHackedTarget defender |> checkAlreadyHackedTarget defender
@ -151,20 +148,21 @@ let hack (target : DiscordUser) (ctx : IDiscordContext) =
}) })
let handleAttack (ctx : IDiscordContext) = let handleAttack (ctx : IDiscordContext) =
Game.executePlayerAction ctx (fun attacker -> async { executePlayerAction ctx (fun attacker -> async {
let tokens = ctx.GetInteractionId().Split("-") let tokens = ctx.GetInteractionId().Split("-")
let hackId = int tokens.[1] 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 resultId , targetId = UInt64.TryParse tokens.[2]
let! resultTarget = DbService.tryFindPlayer GuildEnvironment.pgDb targetId let! resultTarget = DbService.tryFindPlayer targetId
match resultTarget , true , resultId with match resultTarget , true , resultId with
| Some defender , true , true -> | Some defender , true , true ->
do! attacker do! attacker
|> Player.removeExpiredActions |> Player.removeExpiredActions
|> checkAlreadyHackedTarget defender |> checkAlreadyHackedTarget defender
>>= checkPlayerOwnsWeapon hack >>= checkPlayerOwnsWeapon hack.Item
>>= checkWeaponHasCooldown hack >>= checkTargetHasFunds defender
>>= checkWeaponHasCooldown hack.Item
|> function |> function
| Ok atkr -> | Ok atkr ->
runHackerBattle defender hack runHackerBattle defender hack
@ -176,8 +174,8 @@ let handleAttack (ctx : IDiscordContext) =
}) })
let defend (ctx : IDiscordContext) = let defend (ctx : IDiscordContext) =
Game.executePlayerAction ctx (fun player -> async { executePlayerAction ctx (fun player -> async {
if Player.getShields player |> Array.length > 0 then if player.Inventory |> Inventory.getShields |> List.length > 0 then
let p = Player.removeExpiredActions player let p = Player.removeExpiredActions player
let embed = Embeds.pickDefense "Defend" p false let embed = Embeds.pickDefense "Defend" p false
do! ctx.FollowUp embed |> Async.AwaitTask do! ctx.FollowUp embed |> Async.AwaitTask
@ -187,15 +185,15 @@ let defend (ctx : IDiscordContext) =
}) })
let handleDefense (ctx : IDiscordContext) = let handleDefense (ctx : IDiscordContext) =
Game.executePlayerAction ctx (fun player -> async { executePlayerAction ctx (fun player -> async {
let tokens = ctx.GetInteractionId().Split("-") let tokens = ctx.GetInteractionId().Split("-")
let shieldId = int tokens.[1] let shieldId = int tokens.[1]
let shield = Armory.getItem shieldId let shield = Armory.weapons |> Inventory.findShieldById shieldId
do! player do! player
|> checkPlayerOwnsWeapon shield |> checkPlayerOwnsWeapon shield.Item
>>= checkPlayerHasShieldSlotsAvailable shield >>= checkPlayerHasShieldSlotsAvailable
>>= checkWeaponHasCooldown shield >>= checkWeaponHasCooldown shield.Item
|> handleResultWithResponse ctx (fun p -> async { |> handleResultWithResponse ctx (fun p -> async {
let embed = Embeds.responseCreatedShield shield let embed = Embeds.responseCreatedShield shield
do! ctx.FollowUp embed |> Async.AwaitTask do! ctx.FollowUp embed |> Async.AwaitTask
@ -204,9 +202,9 @@ let handleDefense (ctx : IDiscordContext) =
Cooldown = shield.Cooldown Cooldown = shield.Cooldown
Timestamp = DateTime.UtcNow Timestamp = DateTime.UtcNow
} }
do! DbService.updatePlayer GuildEnvironment.pgDb p do! DbService.updatePlayer p
|> Async.Ignore |> Async.Ignore
do! DbService.addPlayerEvent GuildEnvironment.pgDb p.DiscordId defense do! DbService.addPlayerEvent p.DiscordId defense
|> Async.Ignore |> Async.Ignore
let builder = DiscordMessageBuilder() let builder = DiscordMessageBuilder()
builder.WithContent($"{ctx.GetDiscordMember().Username} has protected their system!") |> ignore builder.WithContent($"{ctx.GetDiscordMember().Username} has protected their system!") |> ignore
@ -218,7 +216,7 @@ let handleDefense (ctx : IDiscordContext) =
}) })
let arsenal (ctx : IDiscordContext) = let arsenal (ctx : IDiscordContext) =
Game.executePlayerAction ctx (fun player -> async { executePlayerAction ctx (fun player -> async {
let updatedPlayer = Player.removeExpiredActions player let updatedPlayer = Player.removeExpiredActions player
let builder = DiscordFollowupMessageBuilder() let builder = DiscordFollowupMessageBuilder()
let embed = DiscordEmbedBuilder() let embed = DiscordEmbedBuilder()
@ -226,7 +224,7 @@ let arsenal (ctx : IDiscordContext) =
builder.AddEmbed(embed) |> ignore builder.AddEmbed(embed) |> ignore
builder.IsEphemeral <- true builder.IsEphemeral <- true
do! ctx.FollowUp(builder) |> Async.AwaitTask do! ctx.FollowUp(builder) |> Async.AwaitTask
do! DbService.updatePlayer GuildEnvironment.pgDb updatedPlayer do! DbService.updatePlayer updatedPlayer
|> Async.Ignore |> Async.Ignore
}) })

View File

@ -1,6 +1,5 @@
module Degenz.RockPaperScissors module Degenz.RockPaperScissors
open System
open System.Threading.Tasks open System.Threading.Tasks
open DSharpPlus open DSharpPlus
open DSharpPlus.Entities open DSharpPlus.Entities
@ -85,7 +84,7 @@ let matchResultsEmbed winner move1 move2 player1 player2 =
[ firstEmbed ; secondEmbed ; thirdEmbed ] [ firstEmbed ; secondEmbed ; thirdEmbed ]
let playRPS target ctx = 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 buttons , embed = rpsEmbed false None defender
let builder = let builder =
DiscordFollowupMessageBuilder() DiscordFollowupMessageBuilder()
@ -100,7 +99,7 @@ let handleRPS (ctx : IDiscordContext) =
let move = tokens.[1] let move = tokens.[1]
let targetId = uint64 tokens.[2] let targetId = uint64 tokens.[2]
let isResponse = tokens.[4] = "True" 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 if isResponse then
let eventCtx = ctx.GetContext() :?> ComponentInteractionCreateEventArgs let eventCtx = ctx.GetContext() :?> ComponentInteractionCreateEventArgs
let buttons , embed = rpsEmbed true None attacker let buttons , embed = rpsEmbed true None attacker

View File

@ -1,7 +1,6 @@
module Degenz.SlotMachine module Degenz.SlotMachine
open System open System
open System.Threading.Tasks
open DSharpPlus open DSharpPlus
open DSharpPlus.Entities open DSharpPlus.Entities
open DSharpPlus.SlashCommands open DSharpPlus.SlashCommands
@ -15,7 +14,7 @@ type SlotMachine() =
[<SlashCommand("spin", "Want to try your luck?")>] [<SlashCommand("spin", "Want to try your luck?")>]
member this.Spin (ctx : InteractionContext) = member this.Spin (ctx : InteractionContext) =
Game.executePlayerAction (DiscordInteractionContext ctx) (fun player -> async { PlayerInteractions.executePlayerAction (DiscordInteractionContext ctx) (fun player -> async {
let sleepTime = 1000 let sleepTime = 1000
let random = Random(System.Guid.NewGuid().GetHashCode()) let random = Random(System.Guid.NewGuid().GetHashCode())
let results = [ random.Next(0, 3) ; random.Next(0, 3) ; random.Next(0, 3)] 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]) || (results.[0] <> results.[1] && results.[1] <> results.[2] && results.[0] <> results.[2])
if winConditions then 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 |> Async.Ignore
else 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 |> 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.EventArgs
open DSharpPlus.SlashCommands open DSharpPlus.SlashCommands
open Degenz.Messaging open Degenz.Messaging
open Degenz.PlayerInteractions
let ThiefCooldown = TimeSpan.FromMinutes(1) let ThiefCooldown = TimeSpan.FromMinutes(1)
let VictimRecovery = TimeSpan.FromHours(1) let VictimRecovery = TimeSpan.FromHours(1)
@ -71,7 +72,7 @@ let checkVictimStealingCooldown defender attacker =
defender defender
|> Player.removeExpiredActions |> Player.removeExpiredActions
|> fun p -> p.Events |> fun p -> p.Events
|> Array.tryFind (fun e -> |> List.tryFind (fun e ->
match e.Type with Stealing _ -> true | _ -> false) match e.Type with Stealing _ -> true | _ -> false)
|> function |> function
| Some act -> | Some act ->
@ -81,7 +82,7 @@ let checkVictimStealingCooldown defender attacker =
| None -> Ok attacker | None -> Ok attacker
let checkTargetHasFunds target player = 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." | true -> Error $"Looks like the poor bastard has no $GBT... pick a different victim."
| false -> Ok player | false -> Ok player
@ -94,7 +95,7 @@ let checkThiefCooldown attacker =
attacker attacker
|> Player.removeExpiredActions |> Player.removeExpiredActions
|> fun p -> p.Events |> 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 |> function
| Some act -> | Some act ->
let cooldown = ThiefCooldown - (DateTime.UtcNow - act.Timestamp) let cooldown = ThiefCooldown - (DateTime.UtcNow - act.Timestamp)
@ -119,7 +120,7 @@ let calculateWinPercentage amountRequested bank attackerStrength defenderStrengt
//calculateWinPercentage 50 200 100 85 //calculateWinPercentage 50 200 100 85
let steal target amount (ctx : IDiscordContext) = let steal target amount (ctx : IDiscordContext) =
Game.executePlayerActionWithTarget target ctx (fun thief victim -> async { do! executePlayerActionWithTarget target ctx (fun thief victim -> async { do!
thief thief
|> checkPlayerIsAttackingThemselves victim |> checkPlayerIsAttackingThemselves victim
// |> checkVictimStealingCooldown victim // |> checkVictimStealingCooldown victim
@ -127,7 +128,7 @@ let steal target amount (ctx : IDiscordContext) =
>>= checkPrizeRequestZero amount >>= checkPrizeRequestZero amount
|> handleResultWithResponse ctx (fun _ -> async { |> handleResultWithResponse ctx (fun _ -> async {
let cappedPrize , winPercentage , wasCapped = 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 chance = int (winPercentage * 100.0)
let buttons = 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 cappedMsg = if wasCapped then $"They only have {cappedPrize} $GBT though... " else ""
let strengthMsg = 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 < -50 -> "much stronger"
| diff when diff < 0 -> "stronger" | diff when diff < 0 -> "stronger"
| diff when diff < 50 -> "weaker" | diff when diff < 50 -> "weaker"
@ -160,7 +161,7 @@ let handleSteal (ctx : IDiscordContext) =
let targetId = uint64 tokens.[2] let targetId = uint64 tokens.[2]
let targetName = tokens.[3] let targetName = tokens.[3]
let amount = int tokens.[4] 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 prize = int prize * 1<GBT>
let rand = Random(Guid.NewGuid().GetHashCode()) let rand = Random(Guid.NewGuid().GetHashCode())
@ -174,15 +175,15 @@ let handleSteal (ctx : IDiscordContext) =
| true -> | true ->
let embed = getResultEmbed' Success let embed = getResultEmbed' Success
do! Messaging.sendFollowUpEmbed ctx (embed.Build()) do! Messaging.sendFollowUpEmbed ctx (embed.Build())
match! DbService.tryFindPlayer GuildEnvironment.pgDb targetId with match! DbService.tryFindPlayer targetId with
| Some t -> | Some t ->
let mugged = { let mugged = {
Type = Stealing ( false , thief.basicPlayer ) Type = Stealing ( false , thief.toDiscordPlayer )
Timestamp = DateTime.UtcNow Timestamp = DateTime.UtcNow
Cooldown = VictimRecovery.Minutes * 1<mins> Cooldown = VictimRecovery.Minutes * 1<mins>
} }
do! DbService.updatePlayer GuildEnvironment.pgDb { t with Bank = max (t.Bank - prize) 0<GBT> } |> Async.Ignore do! DbService.updatePlayer { t with Bank = max (t.Bank - prize) 0<GBT> } |> Async.Ignore
do! DbService.addPlayerEvent GuildEnvironment.pgDb victim.DiscordId mugged |> Async.Ignore do! DbService.addPlayerEvent victim.DiscordId mugged |> Async.Ignore
| None -> () | None -> ()
let stole = { let stole = {
@ -190,8 +191,8 @@ let handleSteal (ctx : IDiscordContext) =
Cooldown = ThiefCooldown.Minutes * 1<mins> Cooldown = ThiefCooldown.Minutes * 1<mins>
Timestamp = DateTime.UtcNow Timestamp = DateTime.UtcNow
} }
do! DbService.updatePlayer GuildEnvironment.pgDb { thief with Bank = thief.Bank + prize } |> Async.Ignore do! DbService.updatePlayer { thief with Bank = thief.Bank + prize } |> Async.Ignore
do! DbService.addPlayerEvent GuildEnvironment.pgDb victim.DiscordId stole |> Async.Ignore do! DbService.addPlayerEvent victim.DiscordId stole |> Async.Ignore
let builder = DiscordMessageBuilder() let builder = DiscordMessageBuilder()
builder.WithContent($"{thief.Name} stole {prize} from <@{victim.DiscordId}>!") |> ignore builder.WithContent($"{thief.Name} stole {prize} from <@{victim.DiscordId}>!") |> ignore
let channel = ctx.GetGuild().GetChannel(GuildEnvironment.channelEventsHackerBattle) let channel = ctx.GetGuild().GetChannel(GuildEnvironment.channelEventsHackerBattle)
@ -205,7 +206,7 @@ let handleSteal (ctx : IDiscordContext) =
Cooldown = ThiefCooldown.Minutes * 1<mins> Cooldown = ThiefCooldown.Minutes * 1<mins>
Timestamp = DateTime.UtcNow 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! Messaging.sendFollowUpEmbed ctx (embed.Build())
do! Async.Sleep 2000 do! Async.Sleep 2000
let role = ctx.GetGuild().GetRole(GuildEnvironment.rolePrisoner) let role = ctx.GetGuild().GetRole(GuildEnvironment.rolePrisoner)
@ -219,7 +220,7 @@ let handleSteal (ctx : IDiscordContext) =
} }
if answer = "yes" then if answer = "yes" then
let targetId = uint64 tokens.[2] let targetId = uint64 tokens.[2]
Game.executePlayerActionWithTargetId true targetId ctx (fun attacker defender -> async { executePlayerActionWithTargetId true targetId ctx (fun attacker defender -> async {
do! attacker do! attacker
|> Player.removeExpiredActions |> Player.removeExpiredActions
// |> checkVictimStealingCooldown defender // |> checkVictimStealingCooldown defender

View File

@ -4,27 +4,29 @@ open System.Text
open System.Threading.Tasks open System.Threading.Tasks
open DSharpPlus open DSharpPlus
open DSharpPlus.Entities open DSharpPlus.Entities
open DSharpPlus.EventArgs
open Degenz.Types open Degenz.Types
open Degenz.Messaging open Degenz.Messaging
let trainerAchievement = "FINISHED_TRAINER" let trainerAchievement = "FINISHED_TRAINER"
let Sensei = { Id = GuildEnvironment.botIdHackerBattle ; Name = "Sensei" } let Sensei = { Id = GuildEnvironment.botIdHackerBattle ; Name = "Sensei" }
let defaultHack = Armory.battleItems |> Array.find (fun i -> i.Id = int HackId.Virus) let defaultHack = Armory.weapons |> Inventory.findHackById (int ItemId.Virus)
let defaultShield = Armory.battleItems |> Array.find (fun i -> i.Id = int ShieldId.Firewall) let defaultShield = Armory.weapons |> Inventory.findShieldById (int ItemId.Firewall)
let TrainerEvents = [| let HackEvent () = {
{ Timestamp = System.DateTime.UtcNow Timestamp = System.DateTime.UtcNow
Cooldown = 2<mins> Cooldown = 1<mins>
Type = Hacking { Type = Hacking {
Adversary = Sensei Adversary = Sensei
Success = true Success = true
IsInstigator = true IsInstigator = true
HackId = defaultHack.Id } } HackId = defaultHack.Item.Id
{ Timestamp = System.DateTime.UtcNow }
Cooldown = defaultShield.Cooldown }
Type = Shielding defaultShield.Id } let ShieldEvent () = {
|] Timestamp = System.DateTime.UtcNow
Cooldown = defaultShield.Cooldown
Type = Shielding defaultShield.Item.Id
}
let sendInitialEmbed (client : DiscordClient) = let sendInitialEmbed (client : DiscordClient) =
async { async {
@ -53,7 +55,7 @@ let handleTrainerStep1 (ctx : IDiscordContext) =
|> Async.AwaitTask |> 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" 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" + "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 = let builder =
DiscordInteractionResponseBuilder() DiscordInteractionResponseBuilder()
.WithContent(msg) .WithContent(msg)
@ -67,7 +69,7 @@ let defend (ctx : IDiscordContext) =
do! Messaging.defer ctx do! Messaging.defer ctx
let m = ctx.GetDiscordMember() let m = ctx.GetDiscordMember()
let name = if System.String.IsNullOrEmpty m.Nickname then m.DisplayName else m.Nickname 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 do! ctx.FollowUp(embed) |> Async.AwaitTask
} |> Async.StartAsTask :> Task } |> Async.StartAsTask :> Task
@ -84,17 +86,16 @@ let handleDefense (ctx : IDiscordContext) =
let sendMessage' = sendFollowUpMessage ctx let sendMessage' = sendFollowUpMessage ctx
let tokens = ctx.GetInteractionId().Split("-") let tokens = ctx.GetInteractionId().Split("-")
let shieldId = enum<ShieldId>(int tokens.[2]) let shieldId = enum<ItemId>(int tokens.[2])
let shield = Armory.getItem (int shieldId)
let playerName = tokens.[4] let playerName = tokens.[4]
let embed = Embeds.responseCreatedShield shield let embed = Embeds.responseCreatedShield defaultShield
do! ctx.FollowUp embed |> Async.AwaitTask do! ctx.FollowUp embed |> Async.AwaitTask
do! Async.Sleep 4000 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! Async.Sleep 5000
do! sendMessage' $"❌ HACKING FAILED!\n\n{playerName} defended hack from <@{Sensei.Id}>!" do! sendMessage' $"❌ HACKING FAILED!\n\n{playerName} defended hack from <@{Sensei.Id}>!"
do! Async.Sleep 4000 do! Async.Sleep 4000
do! sendFollowUpMessageWithButton ctx (handleDefenseMsg defaultHack.Name) do! sendFollowUpMessageWithButton ctx (handleDefenseMsg defaultHack.Item.Name)
} |> Async.StartAsTask :> Task } |> Async.StartAsTask :> Task
let handleTrainerStep3 (ctx : IDiscordContext) = let handleTrainerStep3 (ctx : IDiscordContext) =
@ -105,7 +106,7 @@ let handleTrainerStep3 (ctx : IDiscordContext) =
.WithContent .WithContent
( "Now lets **HACK** 💻... I want you to **HACK ME**!\n\n" ( "Now lets **HACK** 💻... I want you to **HACK ME**!\n\n"
+ "To **hack**, you need to run the `/hack` slash command.\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 do! ctx.Respond(InteractionResponseType.ChannelMessageWithSource, builder) |> Async.AwaitTask
} |> Async.StartAsTask :> Task } |> Async.StartAsTask :> Task
@ -117,8 +118,9 @@ let hack (target : DiscordUser) (ctx : IDiscordContext) =
let isRightTarget = target.Id = Sensei.Id let isRightTarget = target.Id = Sensei.Id
match isRightTarget with match isRightTarget with
| true -> | true ->
let player = { PlayerData.empty with Inventory = [ Hack defaultHack ] }
let bot = { PlayerData.empty with DiscordId = Sensei.Id ; Name = Sensei.Name } 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 do! ctx.FollowUp(embed) |> Async.AwaitTask
| false -> | false ->
@ -131,7 +133,7 @@ let hack (target : DiscordUser) (ctx : IDiscordContext) =
} |> Async.StartAsTask :> Task } |> Async.StartAsTask :> Task
let handleHack (ctx : IDiscordContext) = let handleHack (ctx : IDiscordContext) =
Game.executePlayerAction ctx (fun player -> async { PlayerInteractions.executePlayerAction ctx (fun player -> async {
let sendMessage' = sendFollowUpMessage ctx let sendMessage' = sendFollowUpMessage ctx
do! Async.Sleep 1000 do! Async.Sleep 1000
let embed = Embeds.responseSuccessfulHack false Sensei.Id defaultHack.Power defaultHack let embed = Embeds.responseSuccessfulHack false Sensei.Id defaultHack.Power defaultHack
@ -147,12 +149,12 @@ let handleHack (ctx : IDiscordContext) =
let sb = StringBuilder("Here, ") let sb = StringBuilder("Here, ")
let! completed = DbService.checkHasAchievement GuildEnvironment.pgDb player.DiscordId trainerAchievement let! completed = DbService.checkHasAchievement player.DiscordId trainerAchievement
if not completed then if not completed then
do! DbService.addAchievement GuildEnvironment.pgDb player.DiscordId trainerAchievement do! DbService.addAchievement player.DiscordId trainerAchievement
|> Async.Ignore |> 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.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 sb.AppendLine("To finish your training and collect the loot, type the `/arsenal` command **NOW**") |> ignore
do! Async.Sleep 1000 do! Async.Sleep 1000
@ -166,30 +168,50 @@ let handleHack (ctx : IDiscordContext) =
}) })
let handleArsenal (ctx : IDiscordContext) = let handleArsenal (ctx : IDiscordContext) =
Game.executePlayerAction ctx (fun player -> async { PlayerInteractions.executePlayerAction ctx (fun player -> async {
let hasStockWeapons = Player.getHacks player |> Array.exists (fun item -> item.Id = defaultHack.Id) let hack =
let updatedPlayer = if player.Inventory |> List.exists (fun i -> i.Id = defaultHack.Item.Id)
if not hasStockWeapons then { then []
Player.removeExpiredActions player with else [ Hack defaultHack ]
Events = TrainerEvents |> Array.append player.Events let shield =
Inventory = [| defaultHack ; defaultShield |] |> Array.append player.Inventory if player.Inventory |> List.exists (fun i -> i.Id = defaultShield.Item.Id)
} then []
else else [ Shield defaultShield ]
Player.removeExpiredActions player let shieldEvent =
if not hasStockWeapons then let hasShield =
do! player
[ DbService.addPlayerEvent GuildEnvironment.pgDb player.DiscordId TrainerEvents.[0] |> Player.removeExpiredActions
DbService.addPlayerEvent GuildEnvironment.pgDb player.DiscordId TrainerEvents.[1] |> fun p -> p.Events
DbService.updatePlayer GuildEnvironment.pgDb updatedPlayer ] |> List.exists (fun e -> match e.Type with Shielding shieldId -> shieldId = defaultShield.Item.Id | _ -> false)
|> Async.Parallel if hasShield
|> Async.Ignore then []
let embed = Embeds.getArsenalEmbed updatedPlayer else [ ShieldEvent() ]
let updatedPlayer = {
Player.removeExpiredActions player with
Events = shieldEvent @ player.Events
Inventory = hack @ shield @ player.Inventory
}
if not (List.isEmpty hack) || not (List.isEmpty shield) then
do! DbService.updatePlayer updatedPlayer |> Async.Ignore
if not (List.isEmpty shieldEvent) then
try
do! DbService.addPlayerEvent player.DiscordId (List.head shieldEvent) |> Async.Ignore
with ex ->
printfn "%s" ex.Message
()
let playerForEmbed = {
player with
Events = [ HackEvent() ; ShieldEvent() ]
Inventory = hack @ shield @ player.Inventory
}
let embed = Embeds.getArsenalEmbed playerForEmbed
do! ctx.FollowUp(embed) |> Async.AwaitTask 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 if not completed then
do! Async.Sleep 3000 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 let embed = Embeds.getAchievementEmbed rewards "You completed the Training Dojo and collected loot." trainerAchievement
do! ctx.FollowUp(embed) |> Async.AwaitTask do! ctx.FollowUp(embed) |> Async.AwaitTask
do! Async.Sleep 2000 do! Async.Sleep 2000

View File

@ -4,27 +4,34 @@ module Degenz.GuildEnvironment
open System open System
open DSharpPlus.Entities open DSharpPlus.Entities
open dotenv.net open dotenv.net
DotEnv.Load(DotEnvOptions(envFilePaths = [ "../../../../.dev.env" ], overwriteExistingVars = false)) //DotEnv.Load(DotEnvOptions(envFilePaths = [ "../../../../.dev.env" ], overwriteExistingVars = false))
//DotEnv.Load(DotEnvOptions(envFilePaths = [ "../../../../.stag.env" ], overwriteExistingVars = false)) DotEnv.Load(DotEnvOptions(envFilePaths = [ "../../../../.stag.env" ], overwriteExistingVars = false))
//DotEnv.Load(DotEnvOptions(envFilePaths = [ "../../../../.prod.env" ], overwriteExistingVars = false)) //DotEnv.Load(DotEnvOptions(envFilePaths = [ "../../../../.prod.env" ], overwriteExistingVars = false))
let getVar str = Environment.GetEnvironmentVariable(str) let getVar str = Environment.GetEnvironmentVariable(str)
let getId str = getVar str |> uint64 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 guildId = getId "DISCORD_GUILD"
let tokenPlayerInteractions = getVar "TOKEN_PLAYER_INTERACTIONS" let tokenPlayerInteractions = getVar "TOKEN_PLAYER_INTERACTIONS"
let tokenSteal = getVar "TOKEN_STEAL" let tokenSteal = getVar "TOKEN_STEAL"
let tokenHackerBattle = getVar "TOKEN_HACKER_BATTLE" let tokenHackerBattle = getVar "TOKEN_HACKER_BATTLE"
let tokenStore = getVar "TOKEN_STORE" let tokenStore = getVar "TOKEN_STORE"
let tokenInviter = getVar "TOKEN_INVITER"
let channelEventsHackerBattle = getId "CHANNEL_EVENTS_HACKER_BATTLE" let channelEventsHackerBattle = getId "CHANNEL_EVENTS_HACKER_BATTLE"
let channelTraining = getId "CHANNEL_TRAINING" let channelTraining = getId "CHANNEL_TRAINING"
let channelArmory = getId "CHANNEL_ARMORY" let channelArmory = getId "CHANNEL_ARMORY"
//let channelBackAlley = getId "CHANNEL_BACKALLEY"
let channelBattle = getId "CHANNEL_BATTLE" 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 channelThievery = getId "CHANNEL_THIEVERY"
let botIdHackerBattle = getId "BOT_HACKER_BATTLE" let botIdHackerBattle = getId "BOT_HACKER_BATTLE"
let botIdArmory = getId "BOT_ARMORY" let botIdArmory = getId "BOT_ARMORY"
//let botInviter = getId "BOT_INVITER"
let roleTrainee = getId "ROLE_TRAINEE" let roleTrainee = getId "ROLE_TRAINEE"
let rolePrisoner = getId "ROLE_PRISONER" 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 @@
[ [
{ {
"Id": 0, "Case": "Hack",
"Name": "Virus", "Fields": [
"Type": 0, {
"Price": 0, "Power": 25,
"Power": 25, "Class": 0,
"Cooldown": 1, "Cooldown": 1,
"Class": 0, "Item": {
"Attributes": { "Id": 0,
"Sell": false, "Name": "Virus",
"Buy": false, "Price": 0
"Consume": false, }
"Drop": false }
} ]
}, },
{ {
"Id": 1, "Case": "Hack",
"Name": "RemoteAccess", "Fields": [
"Type": 0, {
"Price": 500, "Power": 75,
"Power": 75, "Class": 1,
"Cooldown": 3, "Cooldown": 3,
"Class": 1, "Item": {
"Attributes": { "Id": 1,
"Sell": true, "Name": "Remote Access",
"Buy": true, "Price": 500
"Consume": false, }
"Drop": true }
} ]
}, },
{ {
"Id": 2, "Case": "Hack",
"Name": "Worm", "Fields": [
"Type": 0, {
"Price": 5000, "Power": 150,
"Power": 150, "Class": 2,
"Cooldown": 5, "Cooldown": 5,
"Class": 2, "Item": {
"Attributes": { "Id": 2,
"Sell": true, "Name": "Worm",
"Buy": true, "Price": 5000
"Consume": false, }
"Drop": true }
} ]
}, },
{ {
"Id": 6, "Case": "Shield",
"Name": "Firewall", "Fields": [
"Type": 1, {
"Price": 0, "Class": 0,
"Power": 10, "Cooldown": 120,
"Class": 0, "Item": {
"Cooldown": 120, "Id": 6,
"Attributes": { "Name": "Firewall",
"Sell": false, "Price": 0
"Buy": false, }
"Consume": false, }
"Drop": false ]
}
}, },
{ {
"Id": 7, "Case": "Shield",
"Name": "Encryption", "Fields": [
"Type": 1, {
"Price": 500, "Class": 1,
"Power": 50, "Cooldown": 240,
"Class": 1, "Item": {
"Cooldown": 240, "Id": 7,
"Attributes": { "Name": "Encryption",
"Sell": true, "Price": 500
"Buy": true, }
"Consume": false, }
"Drop": true ]
}
}, },
{ {
"Id": 8, "Case": "Shield",
"Name": "Cypher", "Fields": [
"Type": 1, {
"Price": 5000, "Class": 2,
"Power": 80, "Cooldown": 360,
"Class": 2, "Item": {
"Cooldown": 380, "Id": 8,
"Attributes": { "Name": "Cypher",
"Sell": true, "Price": 5000
"Buy": true, }
"Consume": false, }
"Drop": true ]
} },
{
"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 module Degenz.PlayerInteractions
open System.Threading.Tasks open System.Threading.Tasks
open DSharpPlus.SlashCommands open DSharpPlus
open Degenz.Types open DSharpPlus.Entities
open Degenz.Messaging
open Degenz.DbService
module Commands = let executePlayerAction (ctx : IDiscordContext) (dispatch : PlayerData -> Async<unit>) =
let newPlayer nickname (membr : uint64) = async {
let rand = System.Random(System.Guid.NewGuid().GetHashCode()) let builder = DiscordInteractionResponseBuilder().AsEphemeral(true)
let randHack = rand.Next(0, 3) do! ctx.Respond(InteractionResponseType.DeferredChannelMessageWithSource, builder) |> Async.AwaitTask
let randShield = rand.Next(6, 9) let! playerResult = tryFindPlayer (ctx.GetDiscordMember().Id)
let hack = Armory.battleItems |> Array.find (fun i -> i.Id = randHack) match playerResult with
let shield = Armory.battleItems |> Array.find (fun i -> i.Id = randShield) | Some player -> do! dispatch player
| None -> do! Messaging.sendFollowUpMessage ctx "You are currently not a hacker, first use the /redpill command to become one"
} |> Async.StartAsTask :> Task
{ DiscordId = membr let executePlayerActionWithTarget (targetPlayer : DiscordUser) (ctx : IDiscordContext) (dispatch : PlayerData -> PlayerData -> Async<unit>) =
Name = nickname async {
Inventory = [| hack ; shield |] let builder = DiscordInteractionResponseBuilder()
Events = [||] builder.IsEphemeral <- true
// XP = 0 builder.Content <- "Content"
// Achievements = [||] do! ctx.Respond(InteractionResponseType.DeferredChannelMessageWithSource, builder) |> Async.AwaitTask
Traits = PlayerTraits.empty let! players =
Bank = 100<GBT> } [ tryFindPlayer (ctx.GetDiscordMember().Id)
tryFindPlayer targetPlayer.Id ]
let upsertPlayer discordId = |> Async.Parallel
async { match players.[0] , players.[1] with
let! player = DbService.tryFindPlayer GuildEnvironment.pgDb discordId | Some player , Some target -> do! dispatch player target
let! newPlayer = | None , _ -> do! Messaging.sendFollowUpMessage ctx "You are currently not a hacker, first use the /redpill command to become one"
match player with | _ , None ->
| Some _ -> async.Return false if targetPlayer.IsBot
| None -> then do! Messaging.sendFollowUpMessage ctx $"{targetPlayer.Username} is a bot, pick a real human to hack"
async { else do! Messaging.sendFollowUpMessage ctx "Your target is not connected to the network, they must join first by using the /redpill command"
// do! newPlayer "" discordId |> DbService.insertNewPlayer } |> Async.StartAsTask :> Task
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
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
| 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 = type RewardType =
| Currency of int<GBT> | Currency of int<GBT>
| RandomItem of itemType : ItemType * amount : int // | RandomItem of itemType : ItemType * amount : int
| SpecialItem of id : int | SpecialItem of id : int
[<Literal>] [<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 MinimumVisualStudioVersion = 10.0.40219.1
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Bot", "Bot\Bot.fsproj", "{FF9E58A6-1A1D-4DEC-B52D-265F215BF315}" Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Bot", "Bot\Bot.fsproj", "{FF9E58A6-1A1D-4DEC-B52D-265F215BF315}"
EndProject 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 Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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}.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.ActiveCfg = Release|Any CPU
{FF9E58A6-1A1D-4DEC-B52D-265F215BF315}.Release|Any CPU.Build.0 = 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 EndGlobalSection
EndGlobal 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