547 lines
25 KiB
Forth
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
open FsToolkit.ErrorHandling
let holderRoles = [ GuildEnvironment.roleHolder1 ; GuildEnvironment.roleHolder5 ; GuildEnvironment.roleHolder10 ; GuildEnvironment.roleHolder50 ]
let getRankEmbedColor = function
| 3 -> DiscordColor.Red
| 2 -> DiscordColor.Green
| _ -> DiscordColor.Blurple
let getRankButtonColor = function
| 3 -> ButtonStyle.Danger
| 2 -> ButtonStyle.Success
| _ -> ButtonStyle.Primary
let embedWithError error =
DiscordFollowupMessageBuilder()
.AsEphemeral()
.WithContent(error)
|> Error
let checkHasStock (item : StoreItem) =
if item.Stock > 0 || item.LimitStock = false
then Ok ()
else $"{item.Item.Name} is out of stock! Check back later to purchase"
|> embedWithError
let checkHasSufficientFunds (item : Item) player =
match item.Attributes with
| CanBuy price ->
if player.Bank - price >= 0<GBT>
then Ok ()
else $"""
**__Current Balance:__** {player.Bank} 💰 $GBT
Hold up Degen! You don't have enough $GBT!
Go to <#{GuildEnvironment.channelQuests}> to start earning some now..."""
|> embedWithError
| _ -> $"{item.Name} item cannot be bought"
|> embedWithError
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 $"You own the maximum allowed amount {item.Name}!"
|> embedWithError
else Ok ()
| _ , Some _ -> "You already own this item" |> embedWithError
| _ -> Ok ()
let checkItemSaleStillActive (item : StoreItem) =
match item.SaleEnd with
| Some time ->
let date = DateTimeOffset.FromUnixTimeSeconds(time).DateTime.ToUniversalTime()
if DateTime.UtcNow < date then Ok () else $"Sale for {item.Item.Name} has already ended!" |> embedWithError
| None -> Ok ()
let checkSoldItemAlready (item : Item) player =
if player.Inventory |> List.exists (fun i -> item.Id = i.Id)
then Ok ()
else $"{item.Name} not found in your inventory! Looks like you sold it already."
|> embedWithError
let checkHasItemsInArsenal itemType items =
if List.isEmpty items |> not
then Ok ()
else $"You currently have no {itemType} in your arsenal to sell!"
|> embedWithError
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) |> ignore
elif max < 1000 then embed.AddField($"Max Allowed", string max, true) |> ignore
else ()
// 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 2 else 1) |> string, true) |> ignore
item.SaleEnd |> Option.iter (fun time ->
let date = DateTimeOffset.FromUnixTimeSeconds(time).DateTime.ToUniversalTime()
if DateTime.UtcNow < date then
embed.AddField("⏰ Closes", $"<t:{time}:R>", true) |> ignore
else
embed.AddField("🚫 Closed", $"<t:{time}:R>", true) |> ignore)
item.TotalSold |> Option.iter (fun total -> embed.AddField("Total Sold", string total, true) |> ignore)
embed.Color <- getRankEmbedColor item.Rank
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 saleStillOngoing =
match item.SaleEnd with
| Some time ->
let date = DateTimeOffset.FromUnixTimeSeconds(time).DateTime.ToUniversalTime()
DateTime.UtcNow < date
| None -> true
let inStock = item.Available && (item.Stock > 0 || item.LimitStock = false)
match owned , inStock , saleStillOngoing with
| _ , false , _ ->
let msg = if item.Available then "Out of Stock" else "Unavailable"
DiscordButtonComponent(getRankButtonColor item.Rank, $"Buy-{item.Item.Id}-{storeId}", $"{item.Item.Name} ({msg})", true)
| false , true , true -> DiscordButtonComponent(getRankButtonColor item.Rank, $"Buy-{item.Item.Id}-{storeId}", $"Buy {item.Item.Name}")
| _ , _ , false -> DiscordButtonComponent(getRankButtonColor item.Rank, $"Buy-{item.Item.Id}-{storeId}", $"Closed {item.Item.Name}", true)
| _ ->
match checkDoesntExceedStackCap item.Item player with
| Ok _ -> DiscordButtonComponent(getRankButtonColor item.Rank, $"Buy-{item.Item.Id}-{storeId}", $"Buy {item.Item.Name}")
| Error _ -> DiscordButtonComponent(getRankButtonColor item.Rank, $"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\nWinners announced 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: 20th JUNE__**
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! storeItems = DbService.getAllActiveStoreItems ()
let jpegs =
player.Inventory
|> Inventory.getItemsByType ItemType.Jpeg
|> List.choose (fun ii -> storeItems |> List.tryFind (fun si -> si.Item.Id = ii.Id))
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 : string) (filterBy : ItemType option) (ctx : IDiscordContext) =
executePlayerAction ctx (fun player -> async {
try
let! items =
if storeId.StartsWith("BACKALLEY")
then DbService.getRafflesWithPurchases storeId
else 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
|> List.sortByDescending (fun i -> i.Rank)
let itemStore = getBuyItemsEmbed storeId player items'
do! ctx.FollowUp itemStore |> Async.AwaitTask
do! Analytics.buyItemCommand (ctx.GetDiscordMember()) storeId
else
do! Messaging.sendFollowUpMessage ctx "There are currently no items available, check back later"
with ex -> printfn $"{ex.Message}"
})
let buyForPlayer storeId player (filterBy : ItemType option) (ctx : IDiscordContext) = 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
|> List.sortByDescending (fun i -> i.Rank)
let itemStore = getBuyItemsEmbed storeId player items'
do! ctx.FollowUp itemStore |> Async.AwaitTask
do! Analytics.buyItemCommand (ctx.GetDiscordMember()) storeId
else
do! Messaging.sendFollowUpMessage ctx "There are currently no items available, check back later"
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 with
| Ok _ -> let itemStore = getSellEmbed items
do! ctx.FollowUp(itemStore) |> Async.AwaitTask
| Error e -> do! ctx.FollowUp e |> Async.AwaitTask
do! Analytics.buyItemCommand (ctx.GetDiscordMember()) itemType
})
let checkHasRequiredInvites storeItem player =
async {
match storeItem.RequiresInvites with
| Some amount ->
let! totalInvites = InviteTracker.getInvitedUserCount player.DiscordId
if amount <= totalInvites then
return Ok ()
else
let embedError =
let embed = DiscordEmbedBuilder()
embed.Description <- $"""
**Degen**, cant you **READ**?!
⚠️ **__Entry Requirements:__** {amount}x Invited User
To Enter this Raffle you must have **__INVITED__** {amount} Degen.
☑️ Go to <#{GuildEnvironment.channelRecruitment}>
☑️ Invite just {amount} Degen!
Then try again…
"""
DiscordFollowupMessageBuilder()
.AsEphemeral()
.AddEmbed(embed)
|> Error
return embedError
| None -> return Ok ()
}
let checkHasRequiredRole storeItem (user : DiscordMember) =
match storeItem.RequiresRole with
| Some roleId ->
if holderRoles |> Seq.contains roleId || user.Roles |> Seq.exists (fun r -> r.Id = roleId) then
Ok ()
else
let embed = DiscordEmbedBuilder()
if roleId = GuildEnvironment.roleMagicEden then
embed.Description <- $"""
**Degen**, cant you **READ**?!
⚠️ **__Entry Requirements:__** Have the <@&{GuildEnvironment.roleMagicEden}>
To get the <@&{GuildEnvironment.roleMagicEden}> role:
☑️ Upvote us on magic here: https://magiceden.io/drops/degenz_game
☑️ Post Proof in <#{GuildEnvironment.channelQuestProof}>
Then try again…"""
else
embed.Description <- $"""
**Degen**, cant you **READ**?!
⚠️ **__Entry Requirements:__** Have the <@&{roleId}>
Then try again…
"""
DiscordFollowupMessageBuilder()
.AsEphemeral()
.AddEmbed(embed)
|> Error
| None -> Ok ()
let checkIsHolder storeItem (user : DiscordMember) =
// TODO: Add a check to see if they have a a minimum role
match storeItem.RequiresRole with
| Some roleId ->
if holderRoles |> Seq.contains roleId then
if user.Roles |> Seq.exists (fun r -> holderRoles |> Seq.contains r.Id) then
Ok ()
else
let embed = DiscordEmbedBuilder()
embed.Description <- $"""
**Degen**, cant you **READ**?!
⚠️ **__Entry Requirements:__** Have the <@&{roleId}>
To become a verified holder
1️⃣️ If you still don't own a Degenz Game NFT, head over to https://magiceden.io/marketplace/degenz_game
2️⃣ Verify your wallet <#{GuildEnvironment.channelVerifyNft}>
Then try again…"""
DiscordFollowupMessageBuilder()
.AsEphemeral()
.AddEmbed(embed)
|> Error
else
Ok ()
| None -> Ok ()
// 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! result = asyncResult {
let! storeInventory = DbService.getStoreItems storeId
let storeItem = storeInventory |> List.find (fun si -> si.Item.Id = itemId)
do! checkHasSufficientFunds storeItem.Item player
do! checkHasStock storeItem
do! checkItemSaleStillActive storeItem
do! checkDoesntExceedStackCap storeItem.Item player
do! checkIsHolder storeItem (ctx.GetDiscordMember())
do! checkHasRequiredRole storeItem (ctx.GetDiscordMember())
do! checkHasRequiredInvites storeItem player
return storeItem
}
return!
result
|> handleResultWithEmbed ctx (fun storeItem -> async {
let price = match storeItem.Item.Attributes with CanBuy price -> price | _ -> 0<GBT>
try
do! dispatch ctx |> Async.AwaitTask
do! DbService.updatePlayerCurrency -price player.DiscordId |> Async.Ignore
do! DbService.addToPlayerInventory player.DiscordId storeItem.Item |> Async.Ignore
if storeItem.LimitStock = true && storeItem.Stock > 0 then
do! DbService.decrementItemStock storeItem.Item |> Async.Ignore
let builder = DiscordFollowupMessageBuilder().AsEphemeral(true)
let embed = purchaseItemEmbed 1 storeItem.Item
match storeItem.Item.Attributes , getTotalOwnedOfItem storeItem.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 storeItem.Item, $"Buy-{storeItem.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 {storeItem.Item.Name}!") |> ignore
let channel = ctx.GetGuild().GetChannel(GuildEnvironment.channelEventsHackerBattle)
do! channel.SendMessageAsync(builder)
|> Async.AwaitTask
|> Async.Ignore
do! Analytics.buyItemButton (ctx.GetDiscordMember()) storeItem.Item.Id price
with ex ->
printfn $"STORE ERROR: {ex.Message}"
})
})
let handleSell (ctx : IDiscordContext) itemId =
executePlayerAction ctx (fun player -> async {
let item = player.Inventory |> Inventory.findItemById itemId
do!
player
|> checkSoldItemAlready item
|> handleResultWithEmbed ctx (fun () -> 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 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
}
|> Async.AwaitTask
|> Async.Start
Task.CompletedTask
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 <- "🎟️ Raffle Store"
embed.Color <- DiscordColor.Lilac
embed.Description <- $"Hey, what do you want, kid?\nI aint got all day…"
builder.AddEmbed embed |> ignore
let button3 = DiscordButtonComponent(ButtonStyle.Danger, $"ShowStore-0-BACKALLEY3", $"Holder Raffle") :> DiscordComponent
let button1 = DiscordButtonComponent(ButtonStyle.Success, $"ShowStore-0-BACKALLEY1", $"NFT Raffle") :> DiscordComponent
let button2 = DiscordButtonComponent(ButtonStyle.Success, $"ShowStore-0-BACKALLEY2", $"Whitelist Raffle") :> DiscordComponent
// let button3 = DiscordButtonComponent(ButtonStyle.Success, $"ShowStore-0-BACKALLEY3", $"USDT Raffles") :> DiscordComponent
let button4 = DiscordButtonComponent(ButtonStyle.Primary, $"ShowJpegInventory-0-0", $"My Stash") :> DiscordComponent
builder.AddComponents [| button3 ; button1 ; button2 ; 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://s8.gifyu.com/images/Shop_Degenz_Resampled.gif"
embed.Title <- "Weapons 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