New store flow

This commit is contained in:
Joseph Ferano 2022-02-07 21:53:20 +07:00
parent 36471195aa
commit dcf0bdb174
8 changed files with 316 additions and 309 deletions

View File

@ -4,7 +4,6 @@ open System.Threading.Tasks
open DSharpPlus
open DSharpPlus.SlashCommands
open Degenz
open Degenz.PlayerInteractions
open Degenz.HackerBattle
open Degenz.Store
open Emzi0767.Utilities
@ -12,46 +11,41 @@ open Emzi0767.Utilities
type EmptyGlobalCommandToAvoidFamousDuplicateSlashCommandsBug() = inherit ApplicationCommandModule ()
let playerInteractionsConfig = DiscordConfiguration()
let guild = GuildEnvironment.guildId
let hackerBattleConfig = DiscordConfiguration()
let storeConfig = DiscordConfiguration()
//let slotMachineConfig = DiscordConfiguration()
//hackerBattleConfig.MinimumLogLevel <- Microsoft.Extensions.Logging.LogLevel.Trace
//let configs = [| playerInteractionsConfig ; hackerBattleConfig ; storeConfig ; slotMachineConfig ; |]
let configs = [| playerInteractionsConfig ; hackerBattleConfig ; storeConfig |]
//let configs = [| hackerBattleConfig ; storeConfig ; slotMachineConfig ; |]
let configs = [ hackerBattleConfig ; storeConfig ]
for conf in configs do
conf.TokenType <- TokenType.Bot
conf.Intents <- DiscordIntents.All
let guild = GuildEnvironment.guildId
playerInteractionsConfig.Token <- GuildEnvironment.tokenPlayerInteractions
hackerBattleConfig.Token <- GuildEnvironment.tokenHackerBattle
storeConfig.Token <- GuildEnvironment.tokenStore
//slotMachineConfig.Token <- Environment.GetEnvironmentVariable("BOT_SLOT_MACHINE")
//let playerInteractionsBot = new DiscordClient(playerInteractionsConfig)
let hackerBattleBot = new DiscordClient(hackerBattleConfig)
let storeBot = new DiscordClient(storeConfig)
//let slotMachineBot = new DiscordClient(slotMachineConfig)
//let clients = [| storeBot ; trainerBot ; hackerBattleBot ; playerInteractionsBot ; slotMachineBot |]
//let clients = [| hackerBattleBot ; storeBot ; playerInteractionsBot |]
let clients = [| hackerBattleBot ; storeBot |]
//let clients = [| hackerBattleBot ; storeBot ; slotMachineBot |]
let clients = [ hackerBattleBot ; storeBot ]
//let sc1 = playerInteractionsBot.UseSlashCommands()
let sc3 = hackerBattleBot.UseSlashCommands()
let sc4 = storeBot.UseSlashCommands()
//let sc5 = slotMachineBot.UseSlashCommands()
let sc1 = hackerBattleBot.UseSlashCommands()
let sc2 = storeBot.UseSlashCommands()
//let sc3 = slotMachineBot.UseSlashCommands()
//sc1.RegisterCommands<PlayerInteractions>(guild);
sc3.RegisterCommands<HackerGame>(guild);
sc4.RegisterCommands<Store>(guild);
//sc5.RegisterCommands<SlotMachine>(guild);
sc1.RegisterCommands<HackerGame>(guild);
sc2.RegisterCommands<Store>(guild);
//sc3.RegisterCommands<SlotMachine>(guild);
hackerBattleBot.add_ComponentInteractionCreated(AsyncEventHandler(HackerBattle.handleButtonEvent))
storeBot.add_ComponentInteractionCreated(AsyncEventHandler(Store.handleSellButtonEvents))
storeBot.add_ComponentInteractionCreated(AsyncEventHandler(Store.handleStoreEvents))
let asdf (_ : DiscordClient) (event : DSharpPlus.EventArgs.InteractionCreateEventArgs) =
async {
@ -74,11 +68,10 @@ let run (client : DiscordClient) =
do! client.ConnectAsync () |> Async.AwaitTask
}
Trainer.sendInitialEmbed hackerBattleBot
//Trainer.sendInitialEmbed hackerBattleBot
clients
|> Array.map run
|> Array.toSeq
|> List.map run
|> Async.Sequential
|> Async.RunSynchronously
|> ignore

View File

@ -10,19 +10,19 @@
<Content Include="Items.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="paket.references" />
<Compile Include="GuildEnvironment.fs" />
<Compile Include="Game.fs" />
<Compile Include="Embeds.fs" />
<Compile Include="Store.fs" />
<Compile Include="Trainer.fs" />
<Compile Include="HackerBattle.fs" />
<Compile Include="SlotMachine.fs" />
<Compile Include="PlayerInteractions.fs" />
<Compile Include="Bot.fs" />
<Content Include="paket.references"/>
<Compile Include="GuildEnvironment.fs"/>
<Compile Include="Game.fs"/>
<Compile Include="PlayerInteractions.fs"/>
<Compile Include="Embeds.fs"/>
<Compile Include="Store.fs"/>
<Compile Include="Trainer.fs"/>
<Compile Include="HackerBattle.fs"/>
<Compile Include="SlotMachine.fs"/>
<Compile Include="Bot.fs"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DbService\DbService.fsproj" />
<ProjectReference Include="..\DbService\DbService.fsproj"/>
</ItemGroup>
<Import Project="..\.paket\Paket.Restore.targets" />
<Import Project="..\.paket\Paket.Restore.targets"/>
</Project>

View File

@ -4,35 +4,22 @@ open System
open DSharpPlus.EventArgs
open Degenz.Types
open DSharpPlus.Entities
open AsciiTableFormatter
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 getHackGif = function
| HackId.Virus -> "https://s10.gifyu.com/images/Attack-DegenZ-1.gif"
| HackId.RemoteAccess -> "https://s10.gifyu.com/images/Mind-Control-Degenz-V2-min.gif"
| HackId.Worm -> "https://s10.gifyu.com/images/WormBugAttack_Degenz-min.gif"
| HackId.Virus -> "https://s10.gifyu.com/images/Virus-icon.jpg"
| HackId.RemoteAccess -> "https://s10.gifyu.com/images/Mind-Control-Degenz-V2-min.jpg"
| HackId.Worm -> "https://s10.gifyu.com/images/WormBugAttack_Degenz-min.jpg"
| _ -> hackGif
let getShieldGif = function
| ShieldId.Firewall -> "https://s10.gifyu.com/images/Defense-GIF-1-Degenz-min.gif"
| ShieldId.Encryption -> "https://s10.gifyu.com/images/Encryption-Degenz-V2-1-min.gif"
| ShieldId.Cypher -> "https://s10.gifyu.com/images/Cypher-Smaller.gif"
| ShieldId.Firewall -> "https://s10.gifyu.com/images/Defense-GIF-1-Degenz-1.jpg"
| ShieldId.Encryption -> "https://s10.gifyu.com/images/Encryption-Degenz-V2-1-min.jpg"
| ShieldId.Cypher -> "https://s10.gifyu.com/images/Cypher-Smaller.jpg"
| _ -> shieldGif
let constructEmbed message =
let builder = DiscordEmbedBuilder()
builder.Color <- Optional(DiscordColor.Blurple)
builder.Description <- message
let author = DiscordEmbedBuilder.EmbedAuthor()
author.Name <- "Degenz Hacker Game"
author.Url <- "https://twitter.com/degenzgame"
author.IconUrl <- "https://pbs.twimg.com/profile_images/1473192843359309825/cqjm0VQ4_400x400.jpg"
builder.Author <- author
builder.Build()
let pickDefense actionId player =
let buttons =
Messaging.constructButtons actionId (string player.DiscordId) (Player.shields player)
@ -85,43 +72,73 @@ let responseCreatedShieldTrainer (shield : BattleItem) =
DiscordFollowupMessageBuilder()
.AddEmbed(DiscordEmbedBuilder().WithImageUrl(getShieldGif (enum<ShieldId>(shield.Id))))
.AsEphemeral(true)
.WithContent($"Mounted a {shield.Name} defense for 6 hours")
.WithContent($"Mounted a {shield.Name} defense for {TimeSpan.FromMinutes(int shield.Cooldown).Hours} hours")
let eventSuccessfulHack (event : ComponentInteractionCreateEventArgs) targetId prize =
DiscordMessageBuilder()
.WithContent($"{event.User.Username} successfully hacked <@{targetId}> for a total of {prize} GoodBoyTokenz")
let eventFailedHack (event : ComponentInteractionCreateEventArgs) targetId prize =
// let embed =
// DiscordEmbedBuilder()
// .WithColor(DiscordColor.Blurple)
// .WithDescription("Pick the hack that you want to use")
// .WithImageUrl(hackGif)
//
DiscordMessageBuilder()
.WithContent($"{event.User.Username} successfully hacked <@{targetId}> for a total of {prize} GoodBoyTokenz")
[<CLIMutable>]
type Table = {
Name : string
Cost : string
Class : string
}
let getGoodAgainst = function
| BattleClass.Network -> ( ShieldId.Firewall , HackId.Virus )
| BattleClass.Penetration -> ( ShieldId.Cypher , HackId.RemoteAccess )
| BattleClass.Exploit -> ( ShieldId.Encryption , HackId.Worm )
let storeListing store =
let embeds =
let getBuyItemsEmbed (itemType : ItemType) (store : BattleItem array) =
let embeds , buttons =
store
|> Array.groupBy (fun (bi : BattleItem) -> bi.Type)
|> Array.map (fun ( itemType , items ) ->
let msg =
items
|> Array.map (fun item -> { Name = item.Name ; Cost = string item.Cost ; Class = string item.Class })
|> Formatter.Format
|> sprintf "**%As**\n``` %s ```" itemType
DiscordEmbedBuilder()
.WithDescription(msg)
.Build())
|> Array.filter (fun i -> i.Type = itemType)
|> Array.map (fun item ->
let embed = DiscordEmbedBuilder()
match item.Type with
| Hack ->
embed
.AddField($"Weak Against |", getGoodAgainst item.Class |> fst |> string , true)
.AddField("Cooldown |", $"{TimeSpan.FromMinutes(int item.Cooldown).Minutes} minutes", true)
.WithThumbnail(getHackGif (enum<HackId>(item.Id)))
|> ignore
| Shield ->
embed
.AddField($"Strong Against |", getGoodAgainst item.Class |> snd |> string , true)
.AddField("Active For |", $"{TimeSpan.FromMinutes(int item.Cooldown).Hours} hours", true)
.WithThumbnail(getShieldGif (enum<ShieldId>(item.Id)))
|> ignore
embed
.AddField("Cost 💰", $"{item.Cost} $GBT", true)
.WithColor(Game.getClassEmbedColor item.Class)
.WithTitle($"{item.Name}")
|> ignore
let button = DiscordButtonComponent(Game.getClassButtonColor item.Class, $"Buy-{item.Id}", $"Buy {item.Name}")
embed.Build() , button :> DiscordComponent)
|> Array.unzip
DiscordInteractionResponseBuilder()
DiscordFollowupMessageBuilder()
.AddEmbeds(embeds)
.AddComponents(buttons)
.AsEphemeral(true)
let getSellItemsEmbed (itemType : ItemType) (player : PlayerData) =
let embeds , buttons =
player.Arsenal
|> Array.filter (fun i -> i.Type = itemType)
|> Array.map (fun item ->
let embed = DiscordEmbedBuilder()
match item.Type with
| Hack -> embed.WithThumbnail(getHackGif (enum<HackId>(item.Id))) |> ignore
| Shield -> embed.WithThumbnail(getShieldGif (enum<ShieldId>(item.Id))) |> ignore
embed
.AddField("Sell For 💰", $"{item.Cost} $GBT", true)
.WithColor(Game.getClassEmbedColor item.Class)
.WithTitle($"{item.Name}")
|> 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)

View File

@ -1,18 +1,29 @@
module Degenz.Game
namespace Degenz
open System
open System.Threading.Tasks
open DSharpPlus
open DSharpPlus.Entities
open DSharpPlus.EventArgs
open DSharpPlus.SlashCommands
open Degenz.DbService
open Microsoft.VisualBasic
let HackPrize = 10<GBT>
let ShieldPrize = 5<GBT>
module Game =
let HackPrize = 10<GBT>
let ShieldPrize = 5<GBT>
let executePlayerInteraction (ctx : InteractionContext) (dispatch : PlayerData -> Async<unit>) =
let SameTargetAttackCooldown = System.TimeSpan.FromHours(6)
let getClassButtonColor = function
| Network -> ButtonStyle.Danger
| Exploit -> ButtonStyle.Success
| Penetration -> ButtonStyle.Primary
let getClassEmbedColor = function
| Network -> DiscordColor.Red
| Penetration -> DiscordColor.Blurple
| Exploit -> DiscordColor.Green
let executePlayerInteraction (ctx : InteractionContext) (dispatch : PlayerData -> Async<unit>) =
async {
let builder = DiscordInteractionResponseBuilder()
builder.IsEphemeral <- true
@ -26,8 +37,8 @@ let executePlayerInteraction (ctx : InteractionContext) (dispatch : PlayerData -
} |> Async.StartAsTask
:> Task
// TODO: Create an abstraction for these two helper functions
let executePlayerEvent (event : ComponentInteractionCreateEventArgs) (dispatch : PlayerData -> Async<unit>) =
// TODO: Create an abstraction for these two helper functions
let executePlayerEvent (event : ComponentInteractionCreateEventArgs) (dispatch : PlayerData -> Async<unit>) =
async {
let builder = DiscordInteractionResponseBuilder()
builder.IsEphemeral <- true
@ -41,3 +52,27 @@ let executePlayerEvent (event : ComponentInteractionCreateEventArgs) (dispatch :
} |> Async.StartAsTask
:> Task
module Player =
let hacks (player : PlayerData) = player.Arsenal |> Array.filter (fun bi -> bi.Type = Hack)
let shields (player : PlayerData) = player.Arsenal |> Array.filter (fun bi -> bi.Type = Shield)
let attacks player =
player.Actions
|> Array.filter (fun act -> match act.Type with Attack _ -> true | _ -> false)
let defenses player = player.Actions |> Array.filter (fun act -> match act.Type with Defense -> true | _ -> false)
let removeExpiredActions player =
let actions =
player.Actions
|> Array.filter (fun (act : Action) ->
match act.Type with
// So the player doesnt attack the same player so many times in a row
| Attack _ -> System.DateTime.UtcNow - act.Timestamp < Game.SameTargetAttackCooldown
| Defense ->
let item = Armory.getItem act.ActionId
System.DateTime.UtcNow - act.Timestamp < System.TimeSpan.FromMinutes(int item.Cooldown))
{ player with Actions = actions }
let modifyBank (player : PlayerData) amount = { player with Bank = max (player.Bank + amount) 0<GBT> }
let getAttacksFlat actions = actions |> Array.choose (fun act -> match act.Type with Attack ar -> Some (act,ar.Target,ar.Result) | Defense -> None)

View File

@ -9,24 +9,22 @@ open DSharpPlus.SlashCommands
open Degenz
open Degenz.Messaging
let (>>=) x f = Result.bind f x
let checkIfPlayerIsAttackingThemselves defender attacker =
let checkPlayerIsAttackingThemselves defender attacker =
match attacker.DiscordId = defender.DiscordId with
| true -> Error "You think you're clever? You can't hack yourself, pal."
| false -> Ok attacker
let checkForExistingTarget defenderId attacker =
let checkAlreadyHackedTarget defenderId attacker =
attacker.Actions
|> Player.getAttacksFlat
|> Array.tryFind (fun (_,t,_) -> t.Id = defenderId)
|> function
| Some ( atk , target , _ ) ->
let cooldown = getTimeTillCooldownFinishes Player.SameTargetAttackCooldown atk.Timestamp
Error $"You can only hack the same target once every {Player.SameTargetAttackCooldown.Hours} hours, wait {cooldown} to attempt another hack on {target.Name}."
let cooldown = getTimeTillCooldownFinishes Game.SameTargetAttackCooldown atk.Timestamp
Error $"You can only hack the same target once every {Game.SameTargetAttackCooldown.Hours} hours, wait {cooldown} to attempt another hack on {target.Name}."
| None -> Ok attacker
let checkIfHackHasCooldown hackId attacker =
let checkHackHasCooldown hackId attacker =
let mostRecentHackAttack =
attacker.Actions
|> Array.tryFind (fun a -> a.ActionId = hackId)
@ -41,12 +39,12 @@ let checkIfHackHasCooldown hackId attacker =
let item = Armory.battleItems |> Array.find (fun i -> i.Id = hackId)
Error $"{item.Name} is currently on cooldown, wait {cooldown} to use it again."
let checkIfHasEmptyHacks attacker =
let checkHasEmptyHacks attacker =
match Player.hacks attacker with
| [||] -> Error $"You currently do not have any Hacks to steal 💰$GBT from others. Please go to the <#{GuildEnvironment.channelArmory}> and purchase one."
| _ -> Ok attacker
let checkIfTargetHasMoney (target : PlayerData) attacker =
let checkTargetHasMoney (target : PlayerData) attacker =
if target.Bank < Game.HackPrize
then Error $"{target.Name} does not have enough 💰$GBT to steal from, the broke loser. Pick a different target."
else Ok attacker
@ -116,24 +114,17 @@ let attack (target : DiscordUser) (ctx : InteractionContext) =
| Some defender ->
do! attacker
|> Player.removeExpiredActions
|> checkForExistingTarget defender.DiscordId
>>= checkIfHasEmptyHacks
>>= checkIfTargetHasMoney defender
>>= checkIfPlayerIsAttackingThemselves defender
|> checkAlreadyHackedTarget defender.DiscordId
>>= checkHasEmptyHacks
>>= checkTargetHasMoney defender
>>= checkPlayerIsAttackingThemselves defender
|> function
| Ok _ ->
let embed = Embeds.pickHack "Attack" attacker defender
ctx.FollowUpAsync(embed)
|> Async.AwaitTask
|> Async.Ignore
| Error msg ->
let builder =
DiscordFollowupMessageBuilder()
.WithContent(msg)
.AsEphemeral(true)
ctx.FollowUpAsync(builder)
|> Async.AwaitTask
|> Async.Ignore
| Error msg -> sendFollowUpMessageFromCtx ctx msg
| None -> do! sendFollowUpMessageFromCtx ctx "Your target is not connected to the network, they must join first by using the /redpill command"
})
@ -148,8 +139,8 @@ let handleAttack (event : ComponentInteractionCreateEventArgs) =
| Some defender , true , true ->
do! attacker
|> Player.removeExpiredActions
|> checkForExistingTarget defender.DiscordId
>>= (checkIfHackHasCooldown (int hack))
|> checkAlreadyHackedTarget defender.DiscordId
>>= (checkHackHasCooldown (int hack))
|> function
| Ok _ ->
runHackerBattle defender (Armory.getItem (int hack))
@ -175,17 +166,18 @@ let defend (ctx : InteractionContext) =
let handleDefense (event : ComponentInteractionCreateEventArgs) =
Game.executePlayerEvent event (fun player -> async {
let split = event.Id.Split("-")
let shieldId = enum<ShieldId>(int split.[1])
let shieldId = int split.[1]
let shield = Armory.getItem shieldId
let updatedDefenses = player |> Player.removeExpiredActions |> Player.defenses
let alreadyUsedShield = updatedDefenses |> Array.exists (fun d -> d.ActionId = int shieldId)
let alreadyUsedShield = updatedDefenses |> Array.exists (fun d -> d.ActionId = shieldId)
match alreadyUsedShield , updatedDefenses.Length < 2 with
| false , true ->
let embed = Embeds.responseCreatedShield (Armory.getItem (int shieldId))
let embed = Embeds.responseCreatedShield (Armory.getItem shieldId)
do! event.Interaction.CreateFollowupMessageAsync(embed)
|> Async.AwaitTask
|> Async.Ignore
let defense = { ActionId = int shieldId ; Type = Defense ; Timestamp = DateTime.UtcNow }
let defense = { ActionId = shieldId ; Type = Defense ; Timestamp = DateTime.UtcNow }
do! DbService.updatePlayer <| { player with Actions = Array.append [| defense |] player.Actions }
let builder = DiscordMessageBuilder()
builder.WithContent($"{event.User.Username} has protected their system!") |> ignore
@ -194,24 +186,14 @@ let handleDefense (event : ComponentInteractionCreateEventArgs) =
|> Async.AwaitTask
|> Async.Ignore
| _ , false ->
let builder = DiscordFollowupMessageBuilder()
builder.IsEphemeral <- true
let timestamp = updatedDefenses |> Array.rev |> Array.head |> fun a -> a.Timestamp // This should be the next expiring timestamp
let cooldown = getTimeTillCooldownFinishes (TimeSpan.FromHours(6)) timestamp
builder.Content <- $"You are only allowed two shields at a time. Wait {cooldown} minutes to add another shield"
do! event.Interaction.CreateFollowupMessageAsync(builder)
|> Async.AwaitTask
|> Async.Ignore
let cooldown = getTimeTillCooldownFinishes (TimeSpan.FromMinutes(int shield.Cooldown)) timestamp
do! sendFollowUpMessage event $"You are only allowed two shields at a time. Wait {cooldown} minutes to add another shield"
do! DbService.updatePlayer <| { player with Actions = updatedDefenses }
| true , _ ->
let builder = DiscordFollowupMessageBuilder()
builder.IsEphemeral <- true
let timestamp = updatedDefenses |> Array.find (fun d -> d.ActionId = int shieldId) |> fun a -> a.Timestamp
let cooldown = getTimeTillCooldownFinishes (TimeSpan.FromHours(6)) timestamp
builder.Content <- $"{shieldId} shield is already in use. Wait {cooldown} minutes to use this shield again"
do! event.Interaction.CreateFollowupMessageAsync(builder)
|> Async.AwaitTask
|> Async.Ignore
let cooldown = getTimeTillCooldownFinishes (TimeSpan.FromMinutes(int shield.Cooldown)) timestamp
do! sendFollowUpMessage event $"{shieldId} shield is already in use. Wait {cooldown} minutes to use this shield again"
do! DbService.updatePlayer <| { player with Actions = updatedDefenses }
})

View File

@ -8,7 +8,7 @@
"Class": {
"Case": "Network"
},
"Cost": 100,
"Cost": 50,
"Power": 50,
"Cooldown": 2
},
@ -21,7 +21,7 @@
"Class": {
"Case": "Penetration"
},
"Cost": 100,
"Cost": 50,
"Power": 50,
"Cooldown": 2
},
@ -34,7 +34,7 @@
"Class": {
"Case": "Exploit"
},
"Cost": 100,
"Cost": 50,
"Power": 50,
"Cooldown": 2
},
@ -47,20 +47,7 @@
"Class": {
"Case": "Network"
},
"Cost": 100,
"Power": 50,
"Cooldown": 600
},
{
"Id": 7,
"Name": "Encryption",
"Type": {
"Case": "Shield"
},
"Class": {
"Case": "Exploit"
},
"Cost": 100,
"Cost": 50,
"Power": 50,
"Cooldown": 600
},
@ -73,7 +60,20 @@
"Class": {
"Case": "Penetration"
},
"Cost": 100,
"Cost": 50,
"Power": 50,
"Cooldown": 600
},
{
"Id": 7,
"Name": "Encryption",
"Type": {
"Case": "Shield"
},
"Class": {
"Case": "Exploit"
},
"Cost": 50,
"Power": 50,
"Cooldown": 600
}

View File

@ -9,90 +9,48 @@ open Degenz
open Degenz.Embeds
open Degenz.Messaging
let viewStore (ctx : InteractionContext) =
async {
do! ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, Embeds.storeListing Armory.battleItems)
|> Async.AwaitTask
} |> Async.StartAsTask
:> Task
let handleResultWithResponse ctx fn (player : Result<PlayerData, string>) =
match player with
| Ok p -> fn p
| Error e -> async { do! sendFollowUpMessageFromCtx ctx e }
let buyItem (ctx : InteractionContext) itemId =
Game.executePlayerInteraction ctx (fun player -> async {
let item = Armory.getItem itemId
let newBalance = player.Bank - item.Cost
if newBalance >= 0<GBT> then
let playerHasItem = player.Arsenal |> Array.exists (fun w -> item.Id = w.Id)
if not playerHasItem then
let p = { player with Bank = newBalance ; Arsenal = Array.append [| item |] player.Arsenal }
do! DbService.updatePlayer p
do! sendFollowUpMessageFromCtx ctx $"Successfully purchased {item.Name}! You now have {newBalance} 💰$GBT remaining"
else
do! sendFollowUpMessageFromCtx ctx $"You already own this item!"
else
do! sendFollowUpMessageFromCtx ctx $"You do not have sufficient funds to buy this item! Current balance: {player.Bank} GBT"
})
let handleResultWithResponseFromEvent event fn (player : Result<PlayerData, string>) =
match player with
| Ok p -> fn p
| Error e -> async { do! sendFollowUpMessage event e }
let checkHasSufficientFunds (item : BattleItem) player =
if player.Bank - item.Cost >= 0<GBT>
then Ok player
else Error $"You do not have sufficient funds to buy this item! Current balance: {player.Bank} GBT"
let sell (ctx : InteractionContext) =
Game.executePlayerInteraction ctx (fun player -> async {
let hasInventoryToSell = Array.length player.Arsenal > 0
if hasInventoryToSell then
let builder = DiscordFollowupMessageBuilder()
builder.AddEmbed (constructEmbed "Pick the item you wish to sell.") |> ignore
let checkAlreadyOwnsItem (item : BattleItem) player =
if player.Arsenal |> Array.exists (fun w -> item.Id = w.Id)
then Error $"You already own {item.Name}!"
else Ok player
Array.chunkBySize 5 player.Arsenal
|> Array.iter
(fun wps ->
wps
|> Array.map (fun w -> DiscordButtonComponent(ButtonStyle.Primary, $"{w.Type}-{w.Id}", $"{w.Name}"))
|> Seq.cast<DiscordComponent>
|> builder.AddComponents
|> ignore)
builder.AsEphemeral true |> ignore
let checkSoldItemAlready (item : BattleItem) player =
if player.Arsenal |> 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."
do! ctx.FollowUpAsync(builder)
|> Async.AwaitTask
|> Async.Ignore
else
do! sendFollowUpMessageFromCtx ctx "You currently have no inventory to sell"
})
let checkHasItemsInArsenal itemType player =
if player.Arsenal |> 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 handleBuyItem (ctx : InteractionContext) itemId =
Game.executePlayerInteraction ctx (fun player -> async {
let item = Armory.battleItems |> Array.find (fun w -> w.Id = itemId)
let newBalance = player.Bank - item.Cost
if newBalance >= 0<GBT> then
let playerHasItem = player.Arsenal |> Array.exists (fun w -> item.Id = w.Id)
if not playerHasItem then
let p = { player with Bank = newBalance ; Arsenal = Array.append [| item |] player.Arsenal }
do! DbService.updatePlayer p
do! sendFollowUpMessageFromCtx ctx $"Successfully purchased {item.Name}! You now have {newBalance} 💰$GBT remaining"
else
do! sendFollowUpMessageFromCtx ctx $"You already own this item!"
else
do! sendFollowUpMessageFromCtx ctx $"You do not have sufficient funds to buy this item! Current balance: {player.Bank} GBT"
})
let statusFormat p =
$"**Hacks:** {Player.hacks p |> battleItemFormat}\n
**Shields:** {Player.shields p |> battleItemFormat}\n
**Hack Attacks:**\n{Player.attacks p |> actionFormat}\n
**Active Shields:**\n{Player.defenses p |> actionFormat}"
let handleSellButtonEvents (_ : DiscordClient) (event : ComponentInteractionCreateEventArgs) =
Game.executePlayerEvent event (fun player -> async {
let itemId = int <| event.Id.Split("-").[1]
let item = Armory.getItem itemId
let updatedPlayer = { player with Bank = player.Bank + item.Cost ; Arsenal = player.Arsenal |> Array.filter (fun w -> w.Id <> itemId)}
do! DbService.updatePlayer updatedPlayer
let builder = DiscordFollowupMessageBuilder()
builder.IsEphemeral <- true
builder.Content <- $"Sold {item.Type} {item.Name} for {item.Cost}! Current Balance: {updatedPlayer.Bank}"
do! event.Interaction.CreateFollowupMessageAsync(builder)
|> Async.AwaitTask
|> Async.Ignore
})
let status (ctx : InteractionContext) =
let arsenal (ctx : InteractionContext) =
Game.executePlayerInteraction ctx (fun player -> async {
let updatedPlayer = Player.removeExpiredActions player
let builder = DiscordFollowupMessageBuilder()
let embed = DiscordEmbedBuilder()
embed.AddField("Arsenal", Messaging.statusFormat updatedPlayer) |> ignore
embed.AddField("Arsenal", statusFormat updatedPlayer) |> ignore
builder.AddEmbed(embed) |> ignore
builder.IsEphemeral <- true
do! ctx.FollowUpAsync(builder)
@ -101,23 +59,83 @@ let status (ctx : InteractionContext) =
do! DbService.updatePlayer updatedPlayer
})
let buy (ctx : InteractionContext) itemType =
Game.executePlayerInteraction ctx (fun player -> async {
let itemStore = Embeds.getBuyItemsEmbed itemType Armory.battleItems
do! ctx.Interaction.CreateFollowupMessageAsync(itemStore)
|> Async.AwaitTask
|> Async.Ignore
})
// TODO: Remove active shield when selling
let sell (ctx : InteractionContext) itemType =
Game.executePlayerInteraction ctx (fun player -> async {
match checkHasItemsInArsenal itemType player with
| Ok _ ->
let itemStore = Embeds.getSellItemsEmbed itemType player
do! ctx.FollowUpAsync(itemStore)
|> Async.AwaitTask
|> Async.Ignore
| Error e -> do! sendFollowUpMessageFromCtx ctx e
})
// TODO: When you buy a shield, prompt the user to activate it
let handleBuyItem (event : ComponentInteractionCreateEventArgs) itemId =
Game.executePlayerEvent event (fun player -> async {
let item = Armory.battleItems |> Array.find (fun w -> w.Id = itemId)
do! player
|> checkHasSufficientFunds item
>>= checkAlreadyOwnsItem item
|> handleResultWithResponseFromEvent event (fun player -> async {
let newBalance = player.Bank - item.Cost
let p = { player with Bank = newBalance ; Arsenal = Array.append [| item |] player.Arsenal }
do! DbService.updatePlayer p
do! sendFollowUpMessage event $"Successfully purchased {item.Name}! You now have {newBalance} 💰$GBT remaining"
})
})
let handleSell (event : ComponentInteractionCreateEventArgs) itemId =
Game.executePlayerEvent event (fun player -> async {
let item = Armory.getItem itemId
do! player
|> checkSoldItemAlready item
|> handleResultWithResponseFromEvent event (fun player -> async {
let updatedPlayer = { player with Bank = player.Bank + item.Cost ; Arsenal = player.Arsenal |> Array.filter (fun w -> w.Id <> itemId) }
do! DbService.updatePlayer updatedPlayer
do! sendFollowUpMessage event $"Sold {item.Type} {item.Name} for {item.Cost}! Current Balance: {updatedPlayer.Bank}"
})
})
let handleStoreEvents (_ : DiscordClient) (event : ComponentInteractionCreateEventArgs) =
let itemId = int <| event.Id.Split("-").[1]
match event.Id with
| id when id.StartsWith("Buy") -> handleBuyItem event itemId
| id when id.StartsWith("Sell") -> handleSell event itemId
| _ ->
task {
let builder = DiscordInteractionResponseBuilder()
builder.IsEphemeral <- true
builder.Content <- $"Incorrect Action identifier {event.Id}"
do! event.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, builder)
|> Async.AwaitTask
}
type Store() =
inherit ApplicationCommandModule ()
[<SlashCommand("armory", "View Hacks & Shields available for purchase")>]
member _.Armory (ctx : InteractionContext) = viewStore ctx
[<SlashCommand("arsenal", "Get the Hacks and Shields you own, and which ones are active")>]
member this.Arsenal (ctx : InteractionContext) = status ctx
member this.Arsenal (ctx : InteractionContext) = arsenal ctx
[<SlashCommand("buy-hack", "Purchase a hack attack you can use to earn GoodBoyTokenz")>]
member _.BuyHack (ctx : InteractionContext, [<Option("hack-id", "The ID of the hack you wish to purchase")>] hackId : HackId) =
buyItem ctx (int hackId)
member _.BuyHack (ctx : InteractionContext) = buy ctx ItemType.Hack
[<SlashCommand("buy-shield", "Purchase a hack shield so you can protect your GoodBoyTokenz")>]
member this.BuyShield (ctx : InteractionContext, [<Option("shield-id", "The ID of the shield you wish to purchase")>] shieldId : ShieldId) =
buyItem ctx (int shieldId)
member this.BuyShield (ctx : InteractionContext) = buy ctx ItemType.Shield
[<SlashCommand("sell", "Sell an item in your inventory for GoodBoyTokenz")>]
member this.SellItem (ctx : InteractionContext) = sell ctx
[<SlashCommand("sell-hack", "Sell a hack for GoodBoyTokenz")>]
member this.SellHack (ctx : InteractionContext) = sell ctx ItemType.Hack
[<SlashCommand("sell-shield", "Sell a shield for GoodBoyTokenz")>]
member this.SellShield (ctx : InteractionContext) = sell ctx ItemType.Shield

View File

@ -7,6 +7,10 @@ open DSharpPlus.EventArgs
open DSharpPlus.SlashCommands
open Newtonsoft.Json
[<Microsoft.FSharp.Core.AutoOpen>]
module ResultHelpers =
let (>>=) x f = Result.bind f x
[<Microsoft.FSharp.Core.AutoOpen>]
module Types =
@ -83,32 +87,6 @@ module Armory =
let getItem itemId = battleItems |> Array.find (fun w -> w.Id = itemId)
module Player =
let SameTargetAttackCooldown = TimeSpan.FromHours(2)
let hacks player = player.Arsenal |> Array.filter (fun bi -> bi.Type = Hack)
let shields player = player.Arsenal |> Array.filter (fun bi -> bi.Type = Shield)
let attacks player =
player.Actions
|> Array.filter (fun act -> match act.Type with Attack _ -> true | _ -> false)
let defenses player = player.Actions |> Array.filter (fun act -> match act.Type with Defense -> true | _ -> false)
let removeExpiredActions player =
let actions =
player.Actions
|> Array.filter (fun (act : Action) ->
match act.Type with
// So the player doesnt attack the same player so many times in a row
| Attack _ -> DateTime.UtcNow - act.Timestamp < SameTargetAttackCooldown
| Defense ->
let item = Armory.getItem act.ActionId
DateTime.UtcNow - act.Timestamp < TimeSpan.FromMinutes(int item.Cooldown))
{ player with Actions = actions }
let modifyBank player amount = { player with Bank = max (player.Bank + amount) 0<GBT> }
let getAttacksFlat actions = actions |> Array.choose (fun act -> match act.Type with Attack ar -> Some (act,ar.Target,ar.Result) | Defense -> None)
module Messaging =
type InteractiveMessage = {
ButtonId : string
@ -140,16 +118,10 @@ module Messaging =
| Attack atk -> $"Hacked {atk.Target.Name} at {act.Timestamp.ToShortTimeString()}"
| Defense ->
let item = Armory.getItem act.ActionId
let cooldown = getTimeTillCooldownFinishes (TimeSpan.FromMinutes(int item.Cooldown)) act.Timestamp
let cooldown = getTimeTillCooldownFinishes (System.TimeSpan.FromMinutes(int item.Cooldown)) act.Timestamp
$"{item.Name} active for {cooldown}")
|> String.concat "\n"
let statusFormat p =
$"**Hacks:** {Player.hacks p |> battleItemFormat}\n
**Shields:** {Player.shields p |> battleItemFormat}\n
**Hack Attacks:**\n{Player.attacks p |> actionFormat}\n
**Active Shields:** {Player.defenses p |> actionFormat}"
let constructButtons (actionType: string) (playerInfo: string) (weapons: BattleItem array) =
weapons
|> Array.map (fun w -> DiscordButtonComponent(ButtonStyle.Success, $"{actionType}-{w.Id}-{playerInfo}", $"{w.Name}"))
@ -226,13 +198,3 @@ module Messaging =
do! event.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, builder) |> Async.AwaitTask
}
let sendInteractionEventWithButton (event : ComponentInteractionCreateEventArgs) buttonId msg =
async {
let builder = DiscordInteractionResponseBuilder()
let button = DiscordButtonComponent(ButtonStyle.Success, buttonId, "Got it") :> DiscordComponent
builder.AddComponents [| button |] |> ignore
builder.IsEphemeral <- true
builder.Content <- msg
do! event.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, builder) |> Async.AwaitTask
}