408 lines
20 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 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 $"""
**__Current Balance:__** {player.Bank} 💰 $GBT
Hold up Degen! You don't have enough $GBT!
Go to <#{GuildEnvironment.channelQuests}> to start earning some now..."""
| _ -> 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()
if not owned && item.LimitStock then
embed.AddField("Stock", $"{item.Stock}", true) |> ignore
item.Item.Attributes
|> List.iter (function
| Buyable price ->
if not owned then
embed.AddField("Price", (if price = 0<GBT> then "Free" else $"{price} $GBT"), true) |> ignore
| Attackable power ->
let title = match item.Item.Type with ItemType.Hack -> "Reward" | _ -> "Power"
embed.AddField($"{title}", string power, true) |> ignore
| Classable className ->
let title =
match item.Item.Type with
| ItemType.Hack -> "Weak Against" | ItemType.Shield -> "Defeats"
| _ -> ""
let goodAgainst =
match item.Item.Type with
| ItemType.Hack -> WeaponClass.getGoodAgainst className |> fst |> string
| ItemType.Shield -> WeaponClass.getGoodAgainst className |> snd |> string
| _ -> ""
let weaponName = Arsenal.weapons |> List.find (fun w -> w.Id = goodAgainst) |> fun i -> i.Name
embed.AddField(title, weaponName, true) |> ignore
| RateLimitable time ->
let title =
match item.Item.Type with
| ItemType.Hack -> "Cooldown" | ItemType.Shield -> "Active For"
| _ -> ""
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($"Owned", $"{count}", true)
else embed.AddField($"Max Allowed", $"{max}", true)
|> ignore
// let totalOwned = getTotalOwnedOfItem item.Item (items |> List.map (fun i -> i.Item)) |> Option.defaultValue 1
// titleText <- $"{totalOwned}x " + titleText
| 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
| _ -> ())
if item.Item.Type = ItemType.Whitelist then
embed.AddField("Mint Allowance", (if item.Item.Id = "WHITEOG" then 3 else 2) |> string, true) |> ignore
embed.Color <- WeaponClass.getClassEmbedColor item.Item
embed.Title <- titleText
embed.Description <- item.Item.Description
embed.ImageUrl <- "https://stage.degenz.game/blank-row.png"
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 ->
let msg = if item.Available then "Out of Stock" else "Unavailable"
DiscordButtonComponent(WeaponClass.getClassButtonColor item.Item, $"Buy-{item.Item.Id}-{storeId}", $"{item.Item.Name} ({msg})", 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
| ItemType.Whitelist ->
embed.ImageUrl <- item.ImageUrl
let og = if item.Id = "WHITEOG" then "OG " else ""
embed.Description <- $"""
🎉 Congratulations, you purchased {og}WHITELIST!
**__Mint Day: 31ST MAY__**
Go to <#{GuildEnvironment.channelSubmitWallet}> to input your **Solana Wallet Address**, and confirm your Whitelist."""
| _ -> 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 (dispatch : IDiscordContext -> Task) (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! dispatch ctx |> Async.AwaitTask
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 ->
embed.AddField("Owned", $"{amount + 1}", true) |> ignore
embed.AddField("New $GBT Balance", $"`💰` {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 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 (fun _ -> Task.CompletedTask) ctx itemId
| id when id.StartsWith("ShowStore") -> 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 (fun _ -> Task.CompletedTask) ctx itemId
| id when id.StartsWith("Sell") -> handleSell 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, $"ShowStore-0-BACKALLEY1", $"NFT Raffles") :> DiscordComponent
let button2 = DiscordButtonComponent(ButtonStyle.Success, $"ShowStore-0-BACKALLEY2", $"Whitelist Raffles") :> DiscordComponent
let button3 = DiscordButtonComponent(ButtonStyle.Success, $"ShowStore-0-BACKALLEY3", $"USDT Raffles") :> DiscordComponent
let button4 = DiscordButtonComponent(ButtonStyle.Primary, $"ShowJpegInventory-0-0", $"View My Stash") :> DiscordComponent
builder.AddComponents [| button1 ; button2 ; button3 ; button4 |] |> 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)