429 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 getTotalOwnedOfItem (item : Item) (inventory : Item list) =
inventory
|> List.countBy (fun i -> i.Id)
|> List.tryFind (fst >> ((=) item.Id))
|> Option.map snd
let checkDoesntExceedStackCap (item : Item) player =
let itemCount = getTotalOwnedOfItem item player.Inventory
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 mutable titleText = item.Item.Name
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 ->
if not owned then
values.Add("Price", (if price = 0<GBT> then "Free" else $"{price} $GBT"))
| Attackable power ->
let title = match item.Item.Type with ItemType.Hack -> "Reward" | _ -> "Power"
values.Add($"{title}", string power)
| RateLimitable time ->
match item.Item.Type with
| ItemType.Hack -> ()
| ItemType.Shield ->
let ts = TimeSpan.FromMinutes(int time)
let timeStr = if ts.Hours = 0 then $"{ts.Minutes} mins" else $"{ts.Hours} hours"
values.Add($"Active", timeStr)
| _ -> ()
| Stackable max ->
if owned then
let totalOwned = getTotalOwnedOfItem item.Item (items |> List.map (fun i -> i.Item)) |> Option.defaultValue 1
titleText <- $"{totalOwned}x " + titleText
// 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(10)
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
// TODO: This isn't working, try something else
// if not (String.IsNullOrEmpty item.Item.Description) then
// embed.Url <- item.Item.Description
// embed.Footer <- DiscordEmbedBuilder.EmbedFooter()
// embed.Footer.Text <- item.Item.Description
embed
.WithColor(WeaponClass.getClassEmbedColor item.Item)
.WithTitle(titleText)
|> ignore
if table.Columns.Count > 0 then
let split = table.ToPrettyPrintedString().Split("\n")
let text = split |> Array.skip 1 |> Array.take (split.Length - 3) |> String.concat "\n"
embed.WithDescription($"```{text}```") |> 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 player (storeInventory : StoreItem list) =
let embeds = getItemEmbeds false storeInventory
let buttons =
storeInventory
|> List.map (fun item ->
let owned = player.Inventory |> 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 , _ ->
match checkDoesntExceedStackCap item.Item player with
| Ok _ -> DiscordButtonComponent(WeaponClass.getClassButtonColor item.Item, $"Buy-{item.Item.Id}-{storeId}", $"Buy {item.Item.Name}")
| Error _ -> 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 quantity (item : Item) =
let embed = DiscordEmbedBuilder()
embed.ImageUrl <- item.ImageUrl
embed.Title <- $"Purchased {quantity}x {item.Name}"
match item.Type with
| ItemType.Jpeg ->
let itemName = item.Name.Replace("🎟️", "")
embed.Description <- $"Congratulations! You are in the draw for the {itemName}.\n\nThe winner will be announced soon in <#{GuildEnvironment.channelGiveaway}>"
embed.ImageUrl <- item.ImageUrl
embed.Thumbnail <- DiscordEmbedBuilder.EmbedThumbnail()
embed.Thumbnail.Url <- item.IconUrl
| _ -> 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 })
match jpegs with
| [] -> do! Messaging.sendFollowUpMessage ctx $"You currently do not own any jpegs or raffle tickets. Go to <#{GuildEnvironment.channelBackAlley}> to buy some"
| jpegs ->
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 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)
let embed = purchaseItemEmbed 1 item
match item.Attributes , getTotalOwnedOfItem item player.Inventory |> Option.defaultValue 0 with
| CanStack max , amount ->
// TODO: Maybe find a way to show current amount owned
embed.AddField("Owned", $"{amount + 1}", true) |> ignore
embed.AddField("New $GBT Balance", $"`💰` {player.Bank} `💰` {player.Bank - price} `({price} $GBT)`", true) |> ignore
if amount + 1 < max then
let btn = DiscordButtonComponent(WeaponClass.getClassButtonColor item, $"Buy-{item.Id}-{storeId}", $"Buy Another")
builder.AddComponents(btn) |> ignore
| _ -> ()
builder.AddEmbed(embed) |> ignore
do! ctx.FollowUp builder |> Async.AwaitTask
let builder = DiscordMessageBuilder()
builder.WithContent($"{player.Name} just purchased {item.Name}!") |> ignore
let channel = ctx.GetGuild().GetChannel(GuildEnvironment.channelEventsHackerBattle)
do! channel.SendMessageAsync(builder)
|> Async.AwaitTask
|> Async.Ignore
do! Analytics.buyItemButton (ctx.GetDiscordMember()) item.Id 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.sellItemButton (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 handleJpegEvents _ (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("ShowJpegStore") -> buy storeId None ctx
| id when id.StartsWith("ShowRaffleStore") -> buy storeId None ctx
| id when id.StartsWith("ShowJpegInventory") -> showJpegsEmbed ctx
| _ ->
task {
let builder = DiscordInteractionResponseBuilder()
builder.IsEphemeral <- true
builder.Content <- $"Incorrect Action identifier {id}"
do! ctx.Respond(InteractionResponseType.ChannelMessageWithSource, 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("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 Store"
embed.Color <- DiscordColor.Black
embed.Description <- "Hey, what do you want kid?"
builder.AddEmbed embed |> ignore
let button1 = DiscordButtonComponent(ButtonStyle.Success, $"ShowJpegStore-0-BACKALLEY1", $"NFT Raffles") :> DiscordComponent
let button2 = DiscordButtonComponent(ButtonStyle.Success, $"ShowRaffleStore-0-BACKALLEY2", $"USDT Raffles") :> DiscordComponent
let button3 = DiscordButtonComponent(ButtonStyle.Primary, $"ShowJpegInventory-0-0", $"View My Stash") :> DiscordComponent
builder.AddComponents [| button1 ; button2 ; button3 |] |> ignore
do! GuildEnvironment.botClientJpeg.Value.SendMessageAsync(channel, builder)
|> Async.AwaitTask
|> Async.Ignore
with e ->
printfn $"Error trying to get channel Jpeg Store\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 JpegStore() =
inherit ApplicationCommandModule ()
// [<SlashCommand("jpegs", "Check jpegs or raffle tickets you own")>]
// member this.Inventory (ctx : InteractionContext) =
// showJpegsEmbed (DiscordInteractionContext ctx)