418 lines
20 KiB
Forth

module Degenz.Store
open System
open System.Data
open System.Threading.Tasks
open DSharpPlus.Entities
open DSharpPlus
open DSharpPlus.EventArgs
open DSharpPlus.SlashCommands
open Degenz
open Degenz.Messaging
open Degenz.PlayerInteractions
open DataTablePrettyPrinter
let checkHasStock (item : StoreItem) player =
if item.Stock > 0 || item.LimitStock = false
then Ok player
else Error $"{item.Item.Name} is out of stock! Check back later to purchase"
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 getItemEmbeds owned (items : StoreItem list) =
items
|> List.countBy (fun item -> item.Item.Id)
|> List.map (fun (id,count) -> items |> List.find (fun i -> i.Item.Id = id) , count )
|> List.map (fun (item,count) ->
let embed = DiscordEmbedBuilder()
use table = new DataTable()
table.SetBorder(Border.None)
table.SetShowTableName(false)
let values : ResizeArray<string * string> = ResizeArray()
if not owned && item.LimitStock then
values.Add("Stock", $"{item.Stock}")
item.Item.Attributes
|> List.iter (function
| Buyable price ->
values.Add("Price 💰", (if price = 0<GBT> then "Free" else $"{price} $GBT"))
| Attackable power ->
let title = match item.Item.Type with ItemType.Hack -> "$GBT Reward" | _ -> "Power"
values.Add($"{title}", string power)
| RateLimitable time ->
let title = match item.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"
values.Add($"{title}", timeStr)
| Stackable max ->
if owned
then values.Add($"Total Owned", $"{count}")
else values.Add($"Max Allowed", $"{max}")
| 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"
values.Add($"Effect - Amount", $"{fx}")
| _ -> ())
for title , _ in values do
let column = table.Columns.Add(title)
column.SetWidth(40 / values.Count)
column.SetDataAlignment(TextAlignment.Center)
column.SetHeaderBorder(Border.Bottom)
column.SetDataBorder(Border.Top)
let arr : obj array = values |> Seq.map snd |> Seq.cast<obj> |> Seq.toArray
table.Rows.Add(arr) |> ignore
let split = table.ToPrettyPrintedString().Split("\n")
let text = split |> Array.skip 1 |> Array.take (split.Length - 3) |> String.concat "\n"
embed
.WithColor(WeaponClass.getClassEmbedColor item.Item)
.WithDescription($"```{text}```")
.WithTitle($"{item.Item.Name}")
|> ignore
if String.IsNullOrWhiteSpace(item.Item.IconUrl)
then embed
else embed.WithThumbnail(item.Item.IconUrl))
|> List.map (fun e -> e.Build())
|> Seq.ofList
let getBuyItemsEmbed storeId (playerInventory : Inventory) (storeInventory : StoreItem list) =
let embeds = getItemEmbeds false storeInventory
let buttons =
storeInventory
|> List.map (fun item ->
let owned = playerInventory |> List.exists (fun i -> i.Id = item.Item.Id)
let inStock = item.Available && (item.Stock > 0 || item.LimitStock = false)
match owned , inStock with
| false , true -> DiscordButtonComponent(WeaponClass.getClassButtonColor item.Item, $"Buy-{item.Item.Id}-{storeId}", $"Buy {item.Item.Name}")
| false , false -> DiscordButtonComponent(WeaponClass.getClassButtonColor item.Item, $"Buy-{item.Item.Id}-{storeId}", $"{item.Item.Name} (Out of Stock)", true)
| true , _ -> DiscordButtonComponent(WeaponClass.getClassButtonColor item.Item, $"Buy-{item.Item.Id}-{storeId}", $"Own {item.Item.Name}", true)
:> DiscordComponent)
let builder =
DiscordFollowupMessageBuilder()
.AddEmbeds(embeds)
.AsEphemeral(true)
buttons
|> List.chunkBySize 5
|> List.iter (fun btns -> builder.AddComponents(btns) |> ignore)
builder
let purchaseItemEmbed (item : Item) =
let embed = DiscordEmbedBuilder()
embed.ImageUrl <- item.ImageUrl
embed.Title <- $"Purchased {item.Name}"
match item.Type with
| ItemType.Jpeg ->
if item.Id.Contains "RAFFLE" then
embed.Description <- $"Congratulations! You are in the draw for the {item.Name}. The winner will be announced shortly"
embed.ImageUrl <- item.Description
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
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 showJpegsEmbed (ctx : IDiscordContext) = PlayerInteractions.executePlayerAction ctx (fun player -> async {
let jpegs =
player.Inventory
|> Inventory.getItemsByType ItemType.Jpeg
|> List.map (fun i -> { StoreId = "BACKALLEY" ; Item = i ; Stock = 1 ; LimitStock = false ; Available = true })
let embeds = getItemEmbeds true jpegs
let builder = DiscordFollowupMessageBuilder().AddEmbeds(embeds).AsEphemeral(true)
do! ctx.FollowUp builder |> Async.AwaitTask
})
let buy storeId (filterBy : ItemType option) (ctx : IDiscordContext) =
executePlayerAction ctx (fun player -> async {
try
let! items = DbService.getStoreItems storeId
if items.Length > 0 then
let items' =
match filterBy with
| Some itemType -> items |> List.filter (fun item -> item.Item.Type = itemType)
| None -> items
let itemStore = getBuyItemsEmbed storeId player.Inventory items'
do! ctx.FollowUp itemStore |> Async.AwaitTask
do! Analytics.buyItemCommand (ctx.GetDiscordMember()) storeId
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
})
// TODO: When you buy a shield, prompt the user to activate it
let handleBuyItem (ctx : IDiscordContext) itemId =
executePlayerAction ctx (fun player -> async {
let storeId = ctx.GetInteractionId().Split("-").[2]
let! storeInventory = DbService.getStoreItems storeId
let storeItem = storeInventory |> List.find (fun si -> si.Item.Id = itemId)
let item = storeInventory |> List.map (fun i -> i.Item) |> Inventory.findItemById itemId
do! player
|> checkHasSufficientFunds item
>>= checkHasStock storeItem
>>= checkDoesntExceedStackCap item
|> handleResultWithResponse ctx (fun player -> async {
let price = match item.Attributes with CanBuy price -> price | _ -> 0<GBT>
do! DbService.updatePlayerCurrency -price player.DiscordId |> Async.Ignore
do! DbService.addToPlayerInventory player.DiscordId item |> Async.Ignore
if storeItem.LimitStock = true && storeItem.Stock > 0 then
do! DbService.decrementItemStock item |> Async.Ignore
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 ->
do!
[ DbService.updatePlayerCurrency price player.DiscordId |> Async.Ignore
DbService.removeFromPlayerInventory player.DiscordId item |> Async.Ignore
DbService.removeShieldEvent player.DiscordId itemId |> Async.Ignore
sendFollowUpMessage ctx $"Sold {item.Name} for {price}! New Balance: {player.Bank + price}"
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 ->
// TODO: You have to provide the correct store id here
let items' = items |> List.map (fun i -> { StoreId = i.Id ; Item = i ; Stock = 1 ; LimitStock = false ; Available = true })
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 items' = items |> List.map (fun i -> { StoreId = i.Id ; Item = i ; Stock = 1 ; LimitStock = false ; Available = true })
let embeds = getItemEmbeds true items'
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 _ (event : ComponentInteractionCreateEventArgs) =
let ctx = DiscordEventContext event :> IDiscordContext
let id = ctx.GetInteractionId()
let itemId = id.Split("-").[1]
let storeId = id.Split("-").[2]
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 storeId None ctx
| id when id.StartsWith("ShowHacks") -> buy storeId (Some ItemType.Hack) ctx
| id when id.StartsWith("ShowShields") -> buy storeId (Some ItemType.Shield) ctx
| _ ->
task {
let builder = DiscordInteractionResponseBuilder()
builder.IsEphemeral <- true
builder.Content <- $"Incorrect Action identifier {id}"
do! ctx.Respond(InteractionResponseType.ChannelMessageWithSource, builder) |> Async.AwaitTask
}
let sendBackalleyEmbed (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 <- "Jpeg Alley"
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-BACKALLEY", $"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
let sendArmoryEmbed (ctx : IDiscordContext) =
async {
try
let channel = ctx.GetGuild().GetChannel(GuildEnvironment.channelArmory)
let builder = DiscordMessageBuilder()
let embed = DiscordEmbedBuilder()
embed.ImageUrl <- "https://s10.gifyu.com/images/ezgif.com-gif-maker-1696ae238f96d4dfc1.gif"
embed.Title <- "Armory"
embed.Color <- DiscordColor.Black
embed.Description <- "Buy Shields to protect yourself or buy Hacks to extract $GBT from others"
builder.AddEmbed embed |> ignore
let btn1 = DiscordButtonComponent(ButtonStyle.Success, $"ShowHacks-0-ARMORY", $"Hacks") :> DiscordComponent
let btn2 = DiscordButtonComponent(ButtonStyle.Success, $"ShowShields-0-ARMORY", $"Shields") :> DiscordComponent
builder.AddComponents [| btn1 ; btn2 |] |> ignore
do! GuildEnvironment.botClientStore.Value.SendMessageAsync(channel, builder)
|> Async.AwaitTask
|> Async.Ignore
with e ->
printfn $"Error trying to get channel Armory\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)