Single project sln structure
This commit is contained in:
parent
0804c1a409
commit
9f80069b2f
@ -11,10 +11,12 @@
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="paket.references" />
|
||||
<Compile Include="Prelude.fs" />
|
||||
<Compile Include="GuildEnvironment.fs" />
|
||||
<Compile Include="Game.fs" />
|
||||
<Compile Include="XP.fs" />
|
||||
<Compile Include="DbService.fs" />
|
||||
<Compile Include="PlayerInteractions.fs" />
|
||||
<Compile Include="XP.fs" />
|
||||
<Compile Include="Embeds.fs" />
|
||||
<Compile Include="SlotMachine.fs" />
|
||||
<Compile Include="Thief.fs" />
|
||||
@ -24,8 +26,5 @@
|
||||
<Compile Include="HackerBattle.fs" />
|
||||
<Compile Include="Bot.fs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DbService\DbService.fsproj" />
|
||||
</ItemGroup>
|
||||
<Import Project="..\.paket\Paket.Restore.targets" />
|
||||
</Project>
|
308
Bot/Game.fs
308
Bot/Game.fs
@ -1,10 +1,268 @@
|
||||
namespace Degenz
|
||||
|
||||
open System
|
||||
open System.Threading.Tasks
|
||||
open DSharpPlus
|
||||
open DSharpPlus.Entities
|
||||
open Degenz.DbService
|
||||
open Degenz.Messaging
|
||||
open DSharpPlus.EventArgs
|
||||
open DSharpPlus.SlashCommands
|
||||
open Newtonsoft.Json
|
||||
|
||||
[<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 }
|
||||
|
||||
open Messaging
|
||||
|
||||
module Game =
|
||||
let SameTargetAttackCooldown = System.TimeSpan.FromHours(1)
|
||||
@ -24,52 +282,6 @@ module Game =
|
||||
| 1 -> ( ShieldId.Encryption , HackId.RemoteAccess )
|
||||
| _ -> ( ShieldId.Cypher , HackId.Worm )
|
||||
|
||||
let executePlayerAction (ctx : IDiscordContext) (dispatch : PlayerData -> Async<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
|
||||
|
@ -136,7 +136,7 @@ let failedHack (ctx : IDiscordContext) attacker defender hack =
|
||||
}
|
||||
|
||||
let hack (target : DiscordUser) (ctx : IDiscordContext) =
|
||||
Game.executePlayerActionWithTarget target ctx (fun attacker defender -> async {
|
||||
PlayerInteractions.executePlayerActionWithTarget target ctx (fun attacker defender -> async {
|
||||
do! attacker
|
||||
|> Player.removeExpiredActions
|
||||
|> checkAlreadyHackedTarget defender
|
||||
@ -151,7 +151,7 @@ let hack (target : DiscordUser) (ctx : IDiscordContext) =
|
||||
})
|
||||
|
||||
let handleAttack (ctx : IDiscordContext) =
|
||||
Game.executePlayerAction ctx (fun attacker -> async {
|
||||
PlayerInteractions.executePlayerAction ctx (fun attacker -> async {
|
||||
let tokens = ctx.GetInteractionId().Split("-")
|
||||
let hackId = int tokens.[1]
|
||||
let hack = Armory.getItem hackId
|
||||
@ -176,7 +176,7 @@ let handleAttack (ctx : IDiscordContext) =
|
||||
})
|
||||
|
||||
let defend (ctx : IDiscordContext) =
|
||||
Game.executePlayerAction ctx (fun player -> async {
|
||||
PlayerInteractions.executePlayerAction ctx (fun player -> async {
|
||||
if Player.getShields player |> Array.length > 0 then
|
||||
let p = Player.removeExpiredActions player
|
||||
let embed = Embeds.pickDefense "Defend" p false
|
||||
@ -187,7 +187,7 @@ let defend (ctx : IDiscordContext) =
|
||||
})
|
||||
|
||||
let handleDefense (ctx : IDiscordContext) =
|
||||
Game.executePlayerAction ctx (fun player -> async {
|
||||
PlayerInteractions.executePlayerAction ctx (fun player -> async {
|
||||
let tokens = ctx.GetInteractionId().Split("-")
|
||||
let shieldId = int tokens.[1]
|
||||
let shield = Armory.getItem shieldId
|
||||
@ -218,7 +218,7 @@ let handleDefense (ctx : IDiscordContext) =
|
||||
})
|
||||
|
||||
let arsenal (ctx : IDiscordContext) =
|
||||
Game.executePlayerAction ctx (fun player -> async {
|
||||
PlayerInteractions.executePlayerAction ctx (fun player -> async {
|
||||
let updatedPlayer = Player.removeExpiredActions player
|
||||
let builder = DiscordFollowupMessageBuilder()
|
||||
let embed = DiscordEmbedBuilder()
|
||||
|
@ -1,8 +1,57 @@
|
||||
module Degenz.PlayerInteractions
|
||||
|
||||
open System.Threading.Tasks
|
||||
open DSharpPlus
|
||||
open DSharpPlus.Entities
|
||||
open DSharpPlus.SlashCommands
|
||||
open Degenz.Types
|
||||
open Degenz.Messaging
|
||||
open Degenz.DbService
|
||||
|
||||
let executePlayerAction (ctx : IDiscordContext) (dispatch : PlayerData -> Async<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 Commands =
|
||||
let newPlayer nickname (membr : uint64) =
|
||||
@ -42,27 +91,6 @@ module Commands =
|
||||
Name : string
|
||||
}
|
||||
|
||||
// let leaderboard (ctx : InteractionContext) =
|
||||
// async {
|
||||
// do! ctx.CreateResponseAsync(InteractionResponseType.DeferredChannelMessageWithSource) |> Async.AwaitTask
|
||||
//
|
||||
// let builder = DiscordFollowupMessageBuilder()
|
||||
// builder.IsEphemeral <- true
|
||||
//
|
||||
// let! leaders = DbService.getTopPlayers 10
|
||||
// let content =
|
||||
// leaders
|
||||
// |> Seq.toArray
|
||||
// |> Array.sortByDescending (fun p -> p.Bank)
|
||||
// |> Array.mapi (fun i p -> { Position = string (i + 1) ; Amount = string p.Bank ; Name = p.Name })
|
||||
// |> Formatter.Format
|
||||
// builder.Content <- if not <| String.IsNullOrEmpty content then $"```{content}```" else "There are no active hackers"
|
||||
// do! ctx.Interaction.CreateFollowupMessageAsync(builder)
|
||||
// |> Async.AwaitTask
|
||||
// |> Async.Ignore
|
||||
// } |> Async.StartAsTask
|
||||
// :> Task
|
||||
|
||||
type PlayerInteractions() =
|
||||
inherit ApplicationCommandModule ()
|
||||
|
||||
|
22
Bot/Prelude.fs
Normal file
22
Bot/Prelude.fs
Normal 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
|
||||
|
@ -85,7 +85,7 @@ let matchResultsEmbed winner move1 move2 player1 player2 =
|
||||
[ firstEmbed ; secondEmbed ; thirdEmbed ]
|
||||
|
||||
let playRPS target ctx =
|
||||
Game.executePlayerActionWithTarget target ctx (fun _ defender -> async {
|
||||
PlayerInteractions.executePlayerActionWithTarget target ctx (fun _ defender -> async {
|
||||
let buttons , embed = rpsEmbed false None defender
|
||||
let builder =
|
||||
DiscordFollowupMessageBuilder()
|
||||
@ -100,7 +100,7 @@ let handleRPS (ctx : IDiscordContext) =
|
||||
let move = tokens.[1]
|
||||
let targetId = uint64 tokens.[2]
|
||||
let isResponse = tokens.[4] = "True"
|
||||
Game.executePlayerActionWithTargetId false targetId ctx (fun attacker defender -> async {
|
||||
PlayerInteractions.executePlayerActionWithTargetId false targetId ctx (fun attacker defender -> async {
|
||||
if isResponse then
|
||||
let eventCtx = ctx.GetContext() :?> ComponentInteractionCreateEventArgs
|
||||
let buttons , embed = rpsEmbed true None attacker
|
||||
|
@ -15,7 +15,7 @@ type SlotMachine() =
|
||||
|
||||
[<SlashCommand("spin", "Want to try your luck?")>]
|
||||
member this.Spin (ctx : InteractionContext) =
|
||||
Game.executePlayerAction (DiscordInteractionContext ctx) (fun player -> async {
|
||||
PlayerInteractions.executePlayerAction (DiscordInteractionContext ctx) (fun player -> async {
|
||||
let sleepTime = 1000
|
||||
let random = Random(System.Guid.NewGuid().GetHashCode())
|
||||
let results = [ random.Next(0, 3) ; random.Next(0, 3) ; random.Next(0, 3)]
|
||||
|
@ -29,13 +29,13 @@ let checkHasItemsInArsenal itemType player =
|
||||
else Error $"You currently have no {itemType}s in your arsenal to sell!"
|
||||
|
||||
let buy itemType (ctx : IDiscordContext) =
|
||||
Game.executePlayerAction ctx (fun player -> async {
|
||||
PlayerInteractions.executePlayerAction ctx (fun player -> async {
|
||||
let itemStore = Embeds.getBuyItemsEmbed player itemType Armory.battleItems
|
||||
do! ctx.FollowUp itemStore |> Async.AwaitTask
|
||||
})
|
||||
|
||||
let sell itemType (ctx : IDiscordContext) =
|
||||
Game.executePlayerAction ctx (fun player -> async {
|
||||
PlayerInteractions.executePlayerAction ctx (fun player -> async {
|
||||
match checkHasItemsInArsenal itemType player with
|
||||
| Ok _ -> let itemStore = Embeds.getSellEmbed itemType player
|
||||
do! ctx.FollowUp(itemStore) |> Async.AwaitTask
|
||||
@ -44,7 +44,7 @@ let sell itemType (ctx : IDiscordContext) =
|
||||
|
||||
// TODO: When you buy a shield, prompt the user to activate it
|
||||
let handleBuyItem (ctx : IDiscordContext) itemId =
|
||||
Game.executePlayerAction ctx (fun player -> async {
|
||||
PlayerInteractions.executePlayerAction ctx (fun player -> async {
|
||||
let item = Armory.battleItems |> Array.find (fun w -> w.Id = itemId)
|
||||
do! player
|
||||
|> checkHasSufficientFunds item
|
||||
@ -58,7 +58,7 @@ let handleBuyItem (ctx : IDiscordContext) itemId =
|
||||
})
|
||||
|
||||
let handleSell (ctx : IDiscordContext) itemId =
|
||||
Game.executePlayerAction ctx (fun player -> async {
|
||||
PlayerInteractions.executePlayerAction ctx (fun player -> async {
|
||||
let item = Armory.getItem itemId
|
||||
do!
|
||||
player
|
||||
|
@ -119,7 +119,7 @@ let calculateWinPercentage amountRequested bank attackerStrength defenderStrengt
|
||||
//calculateWinPercentage 50 200 100 85
|
||||
|
||||
let steal target amount (ctx : IDiscordContext) =
|
||||
Game.executePlayerActionWithTarget target ctx (fun thief victim -> async { do!
|
||||
PlayerInteractions.executePlayerActionWithTarget target ctx (fun thief victim -> async { do!
|
||||
thief
|
||||
|> checkPlayerIsAttackingThemselves victim
|
||||
// |> checkVictimStealingCooldown victim
|
||||
@ -219,7 +219,7 @@ let handleSteal (ctx : IDiscordContext) =
|
||||
}
|
||||
if answer = "yes" then
|
||||
let targetId = uint64 tokens.[2]
|
||||
Game.executePlayerActionWithTargetId true targetId ctx (fun attacker defender -> async {
|
||||
PlayerInteractions.executePlayerActionWithTargetId true targetId ctx (fun attacker defender -> async {
|
||||
do! attacker
|
||||
|> Player.removeExpiredActions
|
||||
// |> checkVictimStealingCooldown defender
|
||||
|
@ -131,7 +131,7 @@ let hack (target : DiscordUser) (ctx : IDiscordContext) =
|
||||
} |> Async.StartAsTask :> Task
|
||||
|
||||
let handleHack (ctx : IDiscordContext) =
|
||||
Game.executePlayerAction ctx (fun player -> async {
|
||||
PlayerInteractions.executePlayerAction ctx (fun player -> async {
|
||||
let sendMessage' = sendFollowUpMessage ctx
|
||||
do! Async.Sleep 1000
|
||||
let embed = Embeds.responseSuccessfulHack false Sensei.Id defaultHack.Power defaultHack
|
||||
@ -166,7 +166,7 @@ let handleHack (ctx : IDiscordContext) =
|
||||
})
|
||||
|
||||
let handleArsenal (ctx : IDiscordContext) =
|
||||
Game.executePlayerAction ctx (fun player -> async {
|
||||
PlayerInteractions.executePlayerAction ctx (fun player -> async {
|
||||
let hasStockWeapons = Player.getHacks player |> Array.exists (fun item -> item.Id = defaultHack.Id)
|
||||
let updatedPlayer =
|
||||
if not hasStockWeapons then {
|
||||
|
@ -4,9 +4,6 @@
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="DbService.fs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Shared\Shared.fsproj" />
|
||||
</ItemGroup>
|
||||
|
@ -5,10 +5,6 @@ VisualStudioVersion = 16.0.30114.105
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Bot", "Bot\Bot.fsproj", "{FF9E58A6-1A1D-4DEC-B52D-265F215BF315}"
|
||||
EndProject
|
||||
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "DbService", "DbService\DbService.fsproj", "{B1D3E1CC-451C-42D4-B054-D64E75E1A3B9}"
|
||||
EndProject
|
||||
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Shared", "Shared\Shared.fsproj", "{BD80E85E-87C8-4F5F-941E-7DCAF9D69838}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@ -22,13 +18,5 @@ Global
|
||||
{FF9E58A6-1A1D-4DEC-B52D-265F215BF315}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{FF9E58A6-1A1D-4DEC-B52D-265F215BF315}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{FF9E58A6-1A1D-4DEC-B52D-265F215BF315}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B1D3E1CC-451C-42D4-B054-D64E75E1A3B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B1D3E1CC-451C-42D4-B054-D64E75E1A3B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B1D3E1CC-451C-42D4-B054-D64E75E1A3B9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B1D3E1CC-451C-42D4-B054-D64E75E1A3B9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{BD80E85E-87C8-4F5F-941E-7DCAF9D69838}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{BD80E85E-87C8-4F5F-941E-7DCAF9D69838}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{BD80E85E-87C8-4F5F-941E-7DCAF9D69838}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{BD80E85E-87C8-4F5F-941E-7DCAF9D69838}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
274
Shared/Shared.fs
274
Shared/Shared.fs
@ -7,277 +7,3 @@ 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 }
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user