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 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 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 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 player = 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 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.TotalSold |> Option.iter (fun total -> embed.AddField("Total Sold", string total) |> 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 -> 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) | false , true -> DiscordButtonComponent(WeaponClass.getClassButtonColor item.Item, $"Buy-{item.Item.Id}-{storeId}", $"Buy {item.Item.Name}") | _ -> 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: 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 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 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 player 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**, can’t 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 user.Roles |> Seq.exists (fun r -> r.Id = roleId) then Ok () else let embed = DiscordEmbedBuilder() if roleId = GuildEnvironment.roleMagicEden then embed.Description <- $""" ❌ **Degen**, can’t 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**, can’t you **READ**?! ⚠️ **__Entry Requirements:__** Have the <@&{roleId}> Then try again… """ DiscordFollowupMessageBuilder() .AsEphemeral() .AddEmbed(embed) |> Error | 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! checkDoesntExceedStackCap storeItem.Item player 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 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 ain’t got all day…" builder.AddEmbed embed |> ignore let button1 = DiscordButtonComponent(ButtonStyle.Danger, $"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 [| 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://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