356 lines
16 KiB
Forth

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 getItemEmbeds owned (items : Inventory) =
items
|> List.countBy (fun item -> item.Id)
|> List.map (fun (id,count) -> items |> List.find (fun i -> i.Id = id) , count )
|> List.map (fun (item,count) ->
let embed = DiscordEmbedBuilder()
item.Attributes
|> List.iter (function
| Buyable price -> embed.AddField("Price 💰", (if price = 0<GBT> then "Free" else $"{price} $GBT"), true) |> ignore
| Attackable power ->
let title = match item.Type with ItemType.Hack -> "$GBT Reward" | _ -> "Power"
embed.AddField($"{title} |", string power, true) |> ignore
| RateLimitable time ->
let title = match item.Type with ItemType.Hack -> "Cooldown" | ItemType.Shield -> "Active For" | _ -> "Expires"
let ts = TimeSpan.FromMinutes(int time)
let timeStr = if ts.Hours = 0 then $"{ts.Minutes} mins" else $"{ts.Hours} hours"
embed.AddField($"{title} |", timeStr, true) |> ignore
| Stackable max ->
if owned then
embed.AddField($"Total Owned |", $"{count}", true) |> ignore
else
embed.AddField($"Max Allowed |", $"{max}", true) |> ignore
| Modifiable effects ->
let fx =
effects
|> List.map (fun f ->
match f.Effect with
| Min i -> $"{f.TargetStat} Min + {i}"
| Max i -> $"{f.TargetStat} Max + {i}"
| Add i ->
let str = if i > 0 then "Boost" else "Penalty"
$"{f.TargetStat} {str} + {i}"
| RateMultiplier i -> $"{f.TargetStat} Multiplier - i")
|> String.concat "\n"
embed.AddField($"Effect - Amount ", $"{fx}", true) |> ignore
| _ -> ())
embed
.WithColor(WeaponClass.getClassEmbedColor item)
.WithTitle($"{item.Name}")
|> ignore
match Embeds.getItemIcon item.Id with
| Some url -> embed.WithThumbnail(url)
| None -> if String.IsNullOrWhiteSpace(item.IconUrl) then embed else embed.WithThumbnail(item.IconUrl))
|> List.map (fun e -> e.Build())
|> Seq.ofList
let getBuyItemsEmbed (playerInventory : Inventory) (storeInventory : Inventory) =
let embeds = getItemEmbeds false storeInventory
let buttons =
storeInventory
|> List.map (fun item ->
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}")
:> DiscordComponent)
let builder =
DiscordFollowupMessageBuilder()
.AddEmbeds(embeds)
.AsEphemeral(true)
buttons
|> List.chunkBySize 5
|> List.iter (fun btns -> builder.AddComponents(btns) |> ignore)
builder
let getSellEmbed (items : Inventory) =
let embeds , buttons =
items
|> List.choose (fun item ->
match item.Attributes with
| CanSell price ->
let builder =
DiscordEmbedBuilder()
.AddField("Sell For 💰", $"{price} $GBT", true)
.WithTitle($"{item.Name}")
.WithColor(WeaponClass.getClassEmbedColor item)
.Build()
let button = DiscordButtonComponent(WeaponClass.getClassButtonColor item, $"Sell-{item.Id}", $"Sell {item.Name}") :> DiscordComponent
Some ( builder , button )
| _ -> None)
|> List.unzip
// TODO: We should alert the user that they have no sellable items
DiscordFollowupMessageBuilder()
.AddEmbeds(embeds)
.AddComponents(buttons)
.AsEphemeral(true)
let checkHasSufficientFunds (item : Item) player =
match item.Attributes with
| CanBuy price ->
if player.Bank - price >= 0<GBT>
then Ok player
else Error $"You do not have sufficient funds to buy this item! Current balance: {player.Bank} GBT"
| _ -> Error $"{item.Name} item cannot be bought"
let checkDoesntExceedStackCap (item : Item) player =
let itemCount =
player.Inventory
|> List.countBy (fun i -> i.Id)
|> List.tryFind (fst >> ((=) item.Id))
|> Option.map snd
match item.Attributes , itemCount with
| CanStack max , Some count ->
if count >= max
then Error $"You own the maximum allowed amount {item.Name}!"
else Ok player
| _ , Some _ -> Error $"You already own this item"
| _ -> Ok player
let checkSoldItemAlready (item : 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 (ctx : IDiscordContext) =
executePlayerAction ctx (fun player -> async {
try
let channelId = ctx.GetChannel().Id
let! items = DbService.getStoreItems channelId
if items.Length > 0 then
let itemStore = getBuyItemsEmbed player.Inventory items
do! ctx.FollowUp itemStore |> Async.AwaitTask
let! storeSymbol = DbService.getStoreSymbol channelId
do! Analytics.buyItemCommand (ctx.GetDiscordMember()) storeSymbol
else
do! Messaging.sendFollowUpMessage ctx "This channel doesn't have anything to sell"
with ex -> printfn $"{ex.Message}"
})
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
do! Analytics.buyItemCommand (ctx.GetDiscordMember()) itemType
})
let purchaseItemEmbed (item : Item) =
let embed = DiscordEmbedBuilder()
embed.ImageUrl <- item.IconUrl
embed.Title <- $"Purchased {item.Name}"
match item.Type with
| ItemType.Jpeg ->
if item.Symbol = "RAFFLE" then
embed.Description <- $"Congratulations! You are in the draw for the {item.Name}. The winner will be announced shortly"
else
embed.Description <- $"Congratulations! You own the rights to the {item.Name} NFT. Please create a ticket in the support channel and we will transfer to your wallet"
| _ -> embed.Description <- $"Purchased {item.Name}"
embed
// TODO: When you buy a shield, prompt the user to activate it
let handleBuyItem (ctx : IDiscordContext) itemId =
executePlayerAction ctx (fun player -> async {
let! storeInventory = DbService.getStoreItems (ctx.GetChannel().Id)
let item = storeInventory |> Inventory.findItemById itemId
do! player
|> checkHasSufficientFunds item
>>= checkDoesntExceedStackCap item
|> handleResultWithResponse ctx (fun player -> async {
let price = match item.Attributes with CanBuy price -> price | _ -> 0<GBT>
do! DbService.updatePlayerCurrency -price player |> Async.Ignore
do! DbService.addToPlayerInventory player.DiscordId item |> Async.Ignore
// do! ctx.FollowUp $"Successfully purchased {item.Name}! You now have {player.Bank - price} 💰$GBT remaining"
let builder = DiscordFollowupMessageBuilder().AsEphemeral(true)
builder.AddEmbed(purchaseItemEmbed (item)) |> ignore
do! ctx.FollowUp builder |> Async.AwaitTask
do! Analytics.buyWeaponButton (ctx.GetDiscordMember()) item.Name price
})
})
let handleSell (ctx : IDiscordContext) itemId =
executePlayerAction ctx (fun player -> async {
let item = player.Inventory |> Inventory.findItemById itemId
do!
player
|> checkSoldItemAlready item
|> handleResultWithResponse ctx (fun player -> async {
match item.Attributes with
| CanSell price ->
let updatedPlayer = {
player with
Bank = player.Bank + 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 {price}! Current Balance: {updatedPlayer.Bank}"
Analytics.sellWeaponButton (ctx.GetDiscordMember()) item price ]
|> Async.Parallel
|> Async.Ignore
| _ -> ()
})
})
let consume (ctx : IDiscordContext) = PlayerInteractions.executePlayerAction ctx (fun player -> async {
match player.Inventory |> Inventory.getFoods with
| [] -> do! Messaging.sendFollowUpMessage ctx "You do not have any items to consume"
| items ->
let embeds = getItemEmbeds true items
let builder = DiscordFollowupMessageBuilder().AddEmbeds(embeds).AsEphemeral(true)
do! ctx.FollowUp builder |> Async.AwaitTask
})
let handleConsume (ctx : IDiscordContext) itemId = PlayerInteractions.executePlayerAction ctx (fun player -> async {
let item = player.Inventory |> Inventory.findItemById itemId
match player.Inventory |> Inventory.getFoods with
| [] -> do! Messaging.sendFollowUpMessage ctx "You do not have any items to consume"
| items ->
let embeds = getItemEmbeds true items
let builder = DiscordFollowupMessageBuilder().AddEmbeds(embeds).AsEphemeral(true)
do! ctx.FollowUp builder |> Async.AwaitTask
})
let showJpegsEmbed (ctx : IDiscordContext) = PlayerInteractions.executePlayerAction ctx (fun player -> async {
let jpegs = player.Inventory |> Inventory.getItemsByType ItemType.Jpeg
let embeds = getItemEmbeds true jpegs
let builder = DiscordFollowupMessageBuilder().AddEmbeds(embeds).AsEphemeral(true)
do! ctx.FollowUp builder |> Async.AwaitTask
})
let showStats (ctx : IDiscordContext) = PlayerInteractions.executePlayerAction ctx (fun player -> async {
let embed = DiscordEmbedBuilder()
PlayerStats.stats
|> List.iter (fun statConfig ->
let playerStat = PlayerStats.getPlayerStat statConfig player
let min =
match statConfig.BaseRange.Min = playerStat.ModRange.Min with
| true -> $"{statConfig.BaseRange.Min}"
| false -> $"{statConfig.BaseRange.Min} (+{playerStat.ModRange.Min}) "
let max =
match statConfig.BaseRange.Max = playerStat.ModRange.Max with
| true -> $"{statConfig.BaseRange.Max}"
| false -> $"{statConfig.BaseRange.Max} (+{playerStat.ModRange.Max}) "
let field = $"{min} |---------------| {max}"
embed.AddField(string statConfig.Id , field) |> ignore)
let builder =
DiscordFollowupMessageBuilder()
.AddEmbed(embed)
.AsEphemeral(true)
do! ctx.FollowUp builder |> Async.AwaitTask
})
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
| id when id.StartsWith("Consume") -> handleConsume ctx itemId
| id when id.StartsWith("ShowJpegInventory") -> buy ctx
| _ ->
task {
let builder = DiscordInteractionResponseBuilder()
builder.IsEphemeral <- true
builder.Content <- $"Incorrect Action identifier {id}"
do! ctx.Respond(InteractionResponseType.ChannelMessageWithSource, builder) |> Async.AwaitTask
}
let sendInitialEmbed (ctx : IDiscordContext) =
async {
try
let channel = ctx.GetGuild().GetChannel(GuildEnvironment.channelBackAlley)
let builder = DiscordMessageBuilder()
let embed = DiscordEmbedBuilder()
embed.ImageUrl <- "https://s7.gifyu.com/images/ezgif.com-gif-maker-23203b9dca779ba7cf.gif"
embed.Title <- "Degenz Game"
embed.Color <- DiscordColor.Black
embed.Description <- "Hey, what do you want kid? Did you come alone?"
builder.AddEmbed embed |> ignore
let button = DiscordButtonComponent(ButtonStyle.Success, $"ShowJpegInventory-0", $"Show me your stash") :> DiscordComponent
builder.AddComponents [| button |] |> ignore
do! GuildEnvironment.botClientStore.Value.SendMessageAsync(channel, builder)
|> Async.AwaitTask
|> Async.Ignore
with e ->
printfn $"Error trying to get channel Jpeg Alley\n\n{e.Message}"
} |> Async.RunSynchronously
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) (storeFn : IDiscordContext -> Task) =
// let checkChannel (ctx : IDiscordContext) =
// match ctx.GetChannel().Id with
// | id when id = GuildEnvironment.channelBackAlley -> buy ItemType.Hack ctx
// | id when id = GuildEnvironment.channelArmory -> buy ItemType.Shield ctx
// | id when id = GuildEnvironment.channelMarket -> buy ItemType.Food ctx
// | id when id = GuildEnvironment.channelAccessoryShop -> buy ItemType.Accessory ctx
// | _ ->
// task {
// let msg = $"This channel doesn't have any items to sell. Try <#{GuildEnvironment.channelArmory}>"
// do! Messaging.sendSimpleResponse ctx msg
// }
[<SlashCommand("buy-item", "Purchase an item")>]
member _.BuyItem (ctx : InteractionContext) = buy (DiscordInteractionContext ctx)
[<SlashCommand("buy-hack", "Purchase a hack so you can take money from other Degenz")>]
member _.BuyHack (ctx : InteractionContext) =
enforceChannel (DiscordInteractionContext(ctx)) buy
[<SlashCommand("buy-shield", "Purchase a hack shield so you can protect your GBT")>]
member this.BuyShield (ctx : InteractionContext) =
enforceChannel (DiscordInteractionContext(ctx)) buy
[<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) = consume (DiscordInteractionContext ctx)
[<SlashCommand("jpegs", "Check your inventory")>]
member this.Inventory (ctx : InteractionContext) =
showJpegsEmbed (DiscordInteractionContext ctx)
[<SlashCommand("stats", "Check your stats")>]
member this.Stats (ctx : InteractionContext) =
showStats (DiscordInteractionContext ctx)