From 0d2bfebb2db9ca58aa9cedc56ca924a7c4140880 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Mon, 25 Apr 2022 13:13:02 +0700 Subject: [PATCH 01/10] Start reintroducing RPG stuff. Some clean up --- Bot/DbService.fs | 30 ---------------- Bot/GameTypes.fs | 2 +- Bot/Games/SlotMachine.fs | 58 ++++++++++++++++-------------- Bot/Games/Store.fs | 77 ++++++++++++++++++++++++++++++++-------- Bot/GuildEnvironment.fs | 6 ++-- Bot/Items.json | 14 ++++++++ 6 files changed, 113 insertions(+), 74 deletions(-) diff --git a/Bot/DbService.fs b/Bot/DbService.fs index 6c99a5e..522a50d 100644 --- a/Bot/DbService.fs +++ b/Bot/DbService.fs @@ -34,36 +34,6 @@ let getPlayerEvents (did : uint64) = ) |> Async.AwaitTask -let getLastPlayedSlotFromPlayer (did : uint64) = async { - let! events = - connStr - |> Sql.connect - |> Sql.parameters [ "did", Sql.string (string did) ] - |> Sql.query """ - SELECT player_event.updated_at FROM player_event - JOIN "user" u on u.id = player_event.user_id - WHERE u.discord_id = @did AND event_type = 'PlayingSlot' - """ - |> Sql.executeAsync (fun read -> read.dateTime "updated_at" ) - |> Async.AwaitTask - match events with - | [] -> return None - | es -> return Some (List.head es) -} - -let updateSlotPlayedFromPlayer (did : uint64) = - connStr - |> Sql.connect - |> Sql.parameters [ "did", Sql.string (string did) ] - |> Sql.query """ - WITH usr AS (SELECT id FROM "user" WHERE discord_id = @did) - UPDATE player_event SET updated_at = now() at time zone 'utc' - FROM usr WHERE usr.id = user_id AND player_event.event_type = 'PlayingSlot'; - """ - |> Sql.executeNonQueryAsync - |> Async.AwaitTask - |> Async.Ignore - let updatePlayerStats (player : PlayerData) = connStr |> Sql.connect diff --git a/Bot/GameTypes.fs b/Bot/GameTypes.fs index 5f04ce7..5f85b57 100644 --- a/Bot/GameTypes.fs +++ b/Bot/GameTypes.fs @@ -2,7 +2,6 @@ module Degenz.Types open System -open Degenz [] type mins @@ -102,6 +101,7 @@ type Item = { Id : int Name : string Price : int + MaxAllowed : int Attributes : ItemAttributes } diff --git a/Bot/Games/SlotMachine.fs b/Bot/Games/SlotMachine.fs index aac21e9..37ea635 100644 --- a/Bot/Games/SlotMachine.fs +++ b/Bot/Games/SlotMachine.fs @@ -103,30 +103,6 @@ let reel1 = getReel (fun s -> s.reel1Count) let reel2 = getReel (fun s -> s.reel2Count) let reel3 = getReel (fun s -> s.reel3Count) -let slots = - [| "https://s7.gifyu.com/images/aneye.png" - "https://s7.gifyu.com/images/anonmask.png" - "https://s7.gifyu.com/images/circuitboard.png" - "https://s7.gifyu.com/images/obey.png" - "https://s7.gifyu.com/images/oldtv.png" - "https://s7.gifyu.com/images/pills.png" - "https://s7.gifyu.com/images/pizza0d47578733961746.png" - "https://s7.gifyu.com/images/ramen0515f00869e1f4eb.png" - "https://s7.gifyu.com/images/rat69609f842a0eb9f5.png" - "https://s7.gifyu.com/images/alcohol.png" - "https://s7.gifyu.com/images/bigbrother.png" - "https://s7.gifyu.com/images/sushi.png" |] -// [| "https://s7.gifyu.com/images/A-bottle-of-pills0a3006d0170e08df.png" -// "https://s7.gifyu.com/images/an-eyec362d8152ae2382b.png" -// "https://s7.gifyu.com/images/anon-face-mask6c7624821c89fc08.png" -// "https://s7.gifyu.com/images/a-piece-of-sushi77071d30f60a89c6.png" -// "https://s7.gifyu.com/images/Circuit-board89056017b80f1d13.png" -// "https://s7.gifyu.com/images/OBEYf2a8234109836c03.png" -// "https://s7.gifyu.com/images/old-tv-screendc6bc9d4b6c1fd65.png" -// "https://s7.gifyu.com/images/pizza030ffc00ff50da0e.png" -// "https://s7.gifyu.com/images/ramen08336d448018c98f.png" -// "https://s7.gifyu.com/images/rat14f65f54f0d75036.png" |] - let slotEmojiNames = [| "sushi" "bigbrother" @@ -172,6 +148,36 @@ let resetJackpot amount = |> Sql.executeNonQueryAsync |> Async.AwaitTask +let getLastPlayedSlotFromPlayer (did : uint64) = async { + let! events = + GuildEnvironment.connectionString + |> Sql.connect + |> Sql.parameters [ "did", Sql.string (string did) ] + |> Sql.query """ + SELECT player_event.updated_at FROM player_event + JOIN "user" u on u.id = player_event.user_id + WHERE u.discord_id = @did AND event_type = 'PlayingSlot' + """ + |> Sql.executeAsync (fun read -> read.dateTime "updated_at" ) + |> Async.AwaitTask + match events with + | [] -> return None + | es -> return Some (List.head es) +} + +let updateSlotPlayedFromPlayer (did : uint64) = + GuildEnvironment.connectionString + |> Sql.connect + |> Sql.parameters [ "did", Sql.string (string did) ] + |> Sql.query """ + WITH usr AS (SELECT id FROM "user" WHERE discord_id = @did) + UPDATE player_event SET updated_at = now() at time zone 'utc' + FROM usr WHERE usr.id = user_id AND player_event.event_type = 'PlayingSlot'; + """ + |> Sql.executeNonQueryAsync + |> Async.AwaitTask + |> Async.Ignore + let handlePrizeTable (ctx : IDiscordContext) = task { do! Messaging.defer ctx @@ -321,10 +327,10 @@ let spin multiplier (ctx : IDiscordContext) = } PlayerInteractions.executePlayerAction ctx (fun player -> async { if player.Bank >= playAmount then - match! DbService.getLastPlayedSlotFromPlayer player.DiscordId with + match! getLastPlayedSlotFromPlayer player.DiscordId with | Some timestamp -> if DateTime.UtcNow - timestamp > TimeSpan.FromSeconds(8) then - do! DbService.updateSlotPlayedFromPlayer player.DiscordId + do! updateSlotPlayedFromPlayer player.DiscordId do! execute player else do! Messaging.sendFollowUpMessage ctx "Wait till you finish the current spin!" diff --git a/Bot/Games/Store.fs b/Bot/Games/Store.fs index aa63c08..0b0887e 100644 --- a/Bot/Games/Store.fs +++ b/Bot/Games/Store.fs @@ -182,6 +182,57 @@ let handleStoreEvents (_ : DiscordClient) (event : ComponentInteractionCreateEve do! ctx.Respond(InteractionResponseType.ChannelMessageWithSource, builder) |> Async.AwaitTask } +let showInventoryEmbed (ctx : IDiscordContext) = PlayerInteractions.executePlayerAction ctx (fun player -> async { + let embeds , buttons = + player.Inventory + |> List.map (fun item -> + let embed = DiscordEmbedBuilder() + match item with + | Hack hack -> + embed.AddField($"$GBT Reward |", string hack.Power, true) + .AddField("Cooldown |", $"{TimeSpan.FromMinutes(int hack.Cooldown).Minutes} minutes", true) + .WithColor(DiscordColor.Red) + .WithThumbnail(Embeds.getItemIcon item.Id) + |> ignore + | Shield shield -> + embed.AddField($"Strong against |", WeaponClass.getGoodAgainst shield.Class |> snd |> string, true) +// .AddField($"Defensive Strength |", string item.Power, true) + .AddField("Active For |", $"{TimeSpan.FromMinutes(int shield.Cooldown).Hours} hours", true) + .WithColor(DiscordColor.SapGreen) + .WithThumbnail(Embeds.getItemIcon item.Id) + |> ignore + | Food food -> + embed.AddField($"Stat |", $"{food.TargetStat}", true) + .WithColor(DiscordColor.Azure) + .AddField($"Amount |", $"+{food.BoostAmount}", true) |> ignore + | Accessory accessory -> + embed.AddField($"Stat |", $"{accessory.TargetStat}", true) + .WithColor(DiscordColor.Goldenrod) |> ignore + if accessory.FloorBoost > 0 then + embed.AddField($"Min Boost |", $"+{accessory.FloorBoost}", true) |> ignore + if accessory.CeilBoost > 0 then + embed.AddField($"Max Boost |", $"+{accessory.CeilBoost}", true) |> ignore + embed + .AddField("Price πŸ’°", (if item.Price = 0 then "Free" else $"{item.Price} $GBT"), true) + .WithTitle($"{item.Name}") + |> ignore + let button = +// if playerInventory |> List.exists (fun i -> i.Id = item.Id) +// then DiscordButtonComponent(WeaponClass.getClassButtonColor item, $"Buy-{item.Id}", $"Own {item.Name}", true) +// else DiscordButtonComponent(WeaponClass.getClassButtonColor item, $"Buy-{item.Id}", $"Buy {item.Name}") + DiscordButtonComponent(WeaponClass.getClassButtonColor item, $"Sell-{item.Id}", $"Sell {item.Name}") + ( embed.Build() , button :> DiscordComponent )) + |> List.unzip + + let builder = + DiscordFollowupMessageBuilder() + .AddEmbeds(embeds) +// .AddComponents(buttons) + .AsEphemeral(true) + do! ctx.FollowUp builder |> Async.AwaitTask +}) + + type Store() = inherit ApplicationCommandModule () @@ -194,21 +245,22 @@ type Store() = 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 (Inventory.getItemsByType ItemType.Hack) ctx + | id when id = GuildEnvironment.channelBackAlley -> buy ItemType.Hack ctx | id when id = GuildEnvironment.channelArmory -> buy ItemType.Shield ctx -// | id when id = GuildEnvironment.channelMarket -> buy (Inventory.getItemsByType ItemType.Food) ctx -// | id when id = GuildEnvironment.channelAccessoryShop -> buy (Inventory.getItemsByType ItemType.Accessory) 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" + let msg = $"This channel doesn't have any items to sell. Try <#{GuildEnvironment.channelArmory}>" do! Messaging.sendSimpleResponse ctx msg } -// [] -// member _.BuyItem (ctx : InteractionContext) = checkChannel (DiscordInteractionContext(ctx)) -// + [] + member _.BuyItem (ctx : InteractionContext) = checkChannel (DiscordInteractionContext ctx) + [] member _.BuyHack (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (buy ItemType.Hack) @@ -217,9 +269,6 @@ type Store() = member this.BuyShield (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (buy ItemType.Shield) -// [] -// member this.BuyFood (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (buy (Inventory.getItemsByType ItemType.Food)) -// [] member this.SellHack (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (sell "Hacks" (Inventory.getItemsByType ItemType.Hack)) @@ -228,9 +277,9 @@ type Store() = [] member this.Consume (ctx : InteractionContext) = - enforceChannel (DiscordInteractionContext(ctx)) (sell "Shields" (Inventory.getItemsByType ItemType.Food)) + enforceChannel (DiscordInteractionContext ctx) (sell "Shields" (Inventory.getItemsByType ItemType.Food)) -// [] -// member this.Inventory (ctx : InteractionContext) = -// enforceChannel (DiscordInteractionContext(ctx)) (sell "Shields" (Inventory.getItemsByType ItemType)) + [] + member this.Inventory (ctx : InteractionContext) = + showInventoryEmbed (DiscordInteractionContext ctx) diff --git a/Bot/GuildEnvironment.fs b/Bot/GuildEnvironment.fs index a384b1e..952b12a 100644 --- a/Bot/GuildEnvironment.fs +++ b/Bot/GuildEnvironment.fs @@ -34,9 +34,9 @@ let channelWhitelist = getId "CHANNEL_WHITELIST" //let channelTosserTed = getId "CHANNEL_TOSSERTED" let channelShelters = getId "CHANNEL_SHELTERS" let channelSlots = getId "CHANNEL_SLOTS" -//let channelBackAlley = getId "CHANNEL_BACKALLEY" -//let channelMarket = getId "CHANNEL_MARKET" -//let channelAccessoryShop = getId "CHANNEL_ACCESSORIES" +let channelBackAlley = getId "CHANNEL_BACKALLEY" +let channelMarket = getId "CHANNEL_MARKET" +let channelAccessoryShop = getId "CHANNEL_ACCESSORIES" //let channelThievery = getId "CHANNEL_THIEVERY" let botIdHackerBattle = getId "BOT_HACKER_BATTLE" diff --git a/Bot/Items.json b/Bot/Items.json index 959d33a..0012b85 100644 --- a/Bot/Items.json +++ b/Bot/Items.json @@ -10,6 +10,7 @@ "Id": 0, "Name": "Virus", "Price": 250, + "MaxAllowed": 1, "Attributes": { "CanBuy" : true, "CanSell" : true, @@ -32,6 +33,7 @@ "Id": 1, "Name": "Remote Access", "Price": 250, + "MaxAllowed": 1, "Attributes": { "CanBuy" : true, "CanSell" : true, @@ -54,6 +56,7 @@ "Id": 2, "Name": "Worm", "Price": 250, + "MaxAllowed": 1, "Attributes": { "CanBuy" : true, "CanSell" : true, @@ -75,6 +78,7 @@ "Id": 6, "Name": "Firewall", "Price": 100, + "MaxAllowed": 1, "Attributes": { "CanBuy" : true, "CanSell" : true, @@ -96,6 +100,7 @@ "Id": 7, "Name": "Encryption", "Price": 100, + "MaxAllowed": 1, "Attributes": { "CanBuy" : true, "CanSell" : true, @@ -117,6 +122,7 @@ "Id": 8, "Name": "Cypher", "Price": 100, + "MaxAllowed": 1, "Attributes": { "CanBuy" : true, "CanSell" : true, @@ -138,6 +144,7 @@ "Id": 12, "Name": "Protein Powder", "Price": 50, + "MaxAllowed": 1, "Attributes": { "CanBuy" : true, "CanSell" : false, @@ -159,6 +166,7 @@ "Id": 13, "Name": "Toro Loco", "Price": 50, + "MaxAllowed": 1, "Attributes": { "CanBuy" : true, "CanSell" : false, @@ -180,6 +188,7 @@ "Id": 14, "Name": "Oldports Cigs", "Price": 50, + "MaxAllowed": 1, "Attributes": { "CanBuy" : true, "CanSell" : false, @@ -201,6 +210,7 @@ "Id": 15, "Name": "Moon Pie", "Price": 50, + "MaxAllowed": 1, "Attributes": { "CanBuy" : true, "CanSell" : false, @@ -222,6 +232,7 @@ "Item": { "Id": 20, "Name": "Kettlebell", + "MaxAllowed": 1, "Price": 250, "Attributes": { "CanBuy" : true, @@ -244,6 +255,7 @@ "Item": { "Id": 21, "Name": "Headphones", + "MaxAllowed": 1, "Price": 250, "Attributes": { "CanBuy" : true, @@ -266,6 +278,7 @@ "Item": { "Id": 22, "Name": "Rolox Watch", + "MaxAllowed": 1, "Price": 250, "Attributes": { "CanBuy" : true, @@ -288,6 +301,7 @@ "Item": { "Id": 23, "Name": "Buddha Keychain", + "MaxAllowed": 1, "Price": 250, "Attributes": { "CanBuy" : true, From 8baa8d8f60614be3c8f14025f9fa24561318f653 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Mon, 25 Apr 2022 17:17:04 +0700 Subject: [PATCH 02/10] Basic stats --- Bot/GameHelpers.fs | 20 +++++++++++++++++--- Bot/GameTypes.fs | 5 +++++ Bot/Games/Store.fs | 26 ++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/Bot/GameHelpers.fs b/Bot/GameHelpers.fs index ca6e930..cccf390 100644 --- a/Bot/GameHelpers.fs +++ b/Bot/GameHelpers.fs @@ -88,15 +88,29 @@ module PlayerStats = let Luck = { Id = StatId.Luck ; BaseDecayRate = 4.17 ; BaseRange = Range.normalized } let Charisma = { Id = StatId.Charisma ; BaseDecayRate = 4.17 ; BaseRange = Range.normalized } - let stats = [ Strength ; Focus ; Luck ; Charisma ] + let stats = [ Strength ; Focus ; Charisma ; Luck ] + let getPlayerStat (statConfig : StatConfig) player = + match statConfig.Id with + | StatId.Strength -> player.Stats.Strength + | StatId.Focus -> player.Stats.Focus + | StatId.Charisma -> player.Stats.Charisma + | StatId.Luck -> player.Stats.Luck + | _ -> player.Stats.Luck + let calculateActiveStat statId amount items = let statConfig = stats |> List.find (fun s -> s.Id = statId) // let hoursElapsed = (DateTime.UtcNow - lastRead).Hours // let totalDecay = float hoursElapsed * statConfig.BaseDecayRate let modMinMax = - let min = items |> List.sumBy (fun item -> match item with | Accessory a -> a.FloorBoost | _ -> 0) - let max = items |> List.sumBy (fun item -> match item with | Accessory a -> a.CeilBoost | _ -> 0) + let min = + items + |> List.filter (fun item -> match item with | Accessory a -> a.TargetStat = statId | _ -> false) + |> List.sumBy (fun item -> match item with | Accessory a -> a.FloorBoost | _ -> 0) + let max = + items + |> List.filter (fun item -> match item with | Accessory a -> a.TargetStat = statId | _ -> false) + |> List.sumBy (fun item -> match item with | Accessory a -> a.CeilBoost | _ -> 0) Range.create (statConfig.BaseRange.Min + min) (statConfig.BaseRange.Max + max) let amountAfterDecay = modMinMax |> Range.constrain amount { Id = statId ; Amount = amountAfterDecay ; ModRange = modMinMax ; LastRead = DateTime.UtcNow } diff --git a/Bot/GameTypes.fs b/Bot/GameTypes.fs index 5f85b57..faece8d 100644 --- a/Bot/GameTypes.fs +++ b/Bot/GameTypes.fs @@ -131,6 +131,11 @@ type AccessoryItem = { Item : Item } +type MeleeWeapon = { + BreakChance : float + Item : Item +} + type ItemDetails = | Hack of HackItem | Shield of ShieldItem diff --git a/Bot/Games/Store.fs b/Bot/Games/Store.fs index 0b0887e..b0e97dd 100644 --- a/Bot/Games/Store.fs +++ b/Bot/Games/Store.fs @@ -232,6 +232,28 @@ let showInventoryEmbed (ctx : IDiscordContext) = PlayerInteractions.executePlaye 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 diffSymbol lhs rhs = if lhs < rhs then "-" elif lhs = rhs then "" else "+" + 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 +}) type Store() = inherit ApplicationCommandModule () @@ -283,3 +305,7 @@ type Store() = member this.Inventory (ctx : InteractionContext) = showInventoryEmbed (DiscordInteractionContext ctx) + [] + member this.Stats (ctx : InteractionContext) = + showStats (DiscordInteractionContext ctx) + From 7661c1944a91362f94ed656c48e45363f8208a9c Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Tue, 26 Apr 2022 11:07:47 +0700 Subject: [PATCH 03/10] Move some of the DB stuff --- Bot/Admin.fs | 2 +- Bot/DbService.fs | 37 ---------------------------------- Bot/Games/Store.fs | 2 +- Bot/InviteTracker.fs | 48 +++++++++++++++++++++++++++++++++++++++----- 4 files changed, 45 insertions(+), 44 deletions(-) diff --git a/Bot/Admin.fs b/Bot/Admin.fs index dbead26..ad2fa71 100644 --- a/Bot/Admin.fs +++ b/Bot/Admin.fs @@ -143,7 +143,7 @@ type AdminBot() = [] member this.SetStock (ctx : InteractionContext, [] amount : int64) = - enforceAdmin (DiscordInteractionContext ctx) (InviteTracker.setWhitelistStock (int amount)) + enforceAdmin (DiscordInteractionContext ctx) (InviteTracker.setCurrentWhitelistStock (int amount)) [] member this.SendEmbedToChannel (ctx : InteractionContext, [] embed : InitEmbeds) = diff --git a/Bot/DbService.fs b/Bot/DbService.fs index 522a50d..27da3c9 100644 --- a/Bot/DbService.fs +++ b/Bot/DbService.fs @@ -253,40 +253,3 @@ let getRandomHackablePlayers (did : uint64) = |> Sql.executeAsync (fun read -> {| Id = read.string "discord_id" |> uint64 ; Name = read.string "display_name" |}) |> Async.AwaitTask -let getWhitelistItem () = - connStr - |> Sql.connect - |> Sql.query """ - SELECT stock, price FROM item WHERE symbol = 'WHITELIST' - """ - |> Sql.executeRowAsync (fun read -> {| Stock = read.int "stock" ; Price = (read.int "price") * 1 |}) - |> Async.AwaitTask - -let updateWhitelistStock () = async { - try - do! connStr - |> Sql.connect - |> Sql.query """ - UPDATE item SET stock = stock - 1 WHERE symbol = 'WHITELIST' - """ - |> Sql.executeNonQueryAsync - |> Async.AwaitTask - |> Async.Ignore - return true - with _ -> return false -} - -let setWhitelistStock amount = async { - try - do! connStr - |> Sql.connect - |> Sql.parameters [ ( "amount" , Sql.int amount ) ] - |> Sql.query """ - UPDATE item SET stock = @amount WHERE symbol = 'WHITELIST' - """ - |> Sql.executeNonQueryAsync - |> Async.AwaitTask - |> Async.Ignore - return true - with _ -> return false -} diff --git a/Bot/Games/Store.fs b/Bot/Games/Store.fs index b0e97dd..08b8081 100644 --- a/Bot/Games/Store.fs +++ b/Bot/Games/Store.fs @@ -299,7 +299,7 @@ type Store() = [] member this.Consume (ctx : InteractionContext) = - enforceChannel (DiscordInteractionContext ctx) (sell "Shields" (Inventory.getItemsByType ItemType.Food)) + enforceChannel (DiscordInteractionContext ctx) (sell "Food" (Inventory.getItemsByType ItemType.Food)) [] member this.Inventory (ctx : InteractionContext) = diff --git a/Bot/InviteTracker.fs b/Bot/InviteTracker.fs index 9bd5c45..b28d79e 100644 --- a/Bot/InviteTracker.fs +++ b/Bot/InviteTracker.fs @@ -178,6 +178,44 @@ let getInvitedUserCount userId = |> Sql.executeRowAsync (fun read -> read.int "count") |> Async.AwaitTask +let getWhitelistItem () = + connStr + |> Sql.connect + |> Sql.query """ + SELECT stock, price FROM item WHERE symbol = 'WHITELIST' + """ + |> Sql.executeRowAsync (fun read -> {| Stock = read.int "stock" ; Price = (read.int "price") * 1 |}) + |> Async.AwaitTask + +let updateWhitelistStock () = async { + try + do! connStr + |> Sql.connect + |> Sql.query """ + UPDATE item SET stock = stock - 1 WHERE symbol = 'WHITELIST' + """ + |> Sql.executeNonQueryAsync + |> Async.AwaitTask + |> Async.Ignore + return true + with _ -> return false +} + +let setWhitelistStock amount = async { + try + do! connStr + |> Sql.connect + |> Sql.parameters [ ( "amount" , Sql.int amount ) ] + |> Sql.query """ + UPDATE item SET stock = @amount WHERE symbol = 'WHITELIST' + """ + |> Sql.executeNonQueryAsync + |> Async.AwaitTask + |> Async.Ignore + return true + with _ -> return false +} + let guildInviteEmbed = let rewardMsg = $"**Your Mission:**\nCLICK THE BUTTON below, then share your **UNIQUE LINK** with any Degenz you want to invite into the Server.\n\n" + @@ -431,7 +469,7 @@ let handleGimmeWhitelist (ctx : IDiscordContext) = let builder = DiscordFollowupMessageBuilder().AsEphemeral(true) - let! wlItem = DbService.getWhitelistItem () + let! wlItem = getWhitelistItem () let! availability = tryGrantWhitelist ctx wlItem.Stock wlItem.Price match availability with | NotAHacker -> whitelistEmbed.Description <- notAHackerMsg @@ -480,7 +518,7 @@ let handleBuyWhitelist (ctx : IDiscordContext) = let builder = DiscordInteractionResponseBuilder().AsEphemeral(true) do! ctx.Respond(InteractionResponseType.DeferredChannelMessageWithSource, builder) - let! wlItem = DbService.getWhitelistItem () + let! wlItem = getWhitelistItem () let builder = DiscordFollowupMessageBuilder().AsEphemeral(true) match! tryGrantWhitelist ctx wlItem.Stock wlItem.Price with | NotAHacker -> @@ -499,7 +537,7 @@ let handleBuyWhitelist (ctx : IDiscordContext) = builder.Content <- $"We just ran out of stock, tough shit" do! ctx.FollowUp(builder) | Granted player -> - match! DbService.updateWhitelistStock () with + match! updateWhitelistStock () with | true -> let embed = DiscordEmbedBuilder() embed.Description <- buyWhitelistMsg @@ -588,10 +626,10 @@ let handleGuildMemberAdded _ (eventArgs : GuildMemberAddEventArgs) = do! processNewUser eventArgs } :> Task -let rec setWhitelistStock amount (ctx : IDiscordContext) = +let setCurrentWhitelistStock amount (ctx : IDiscordContext) = task { do! Messaging.defer ctx - let! result = DbService.setWhitelistStock amount + let! result = setWhitelistStock amount if result then do! Messaging.sendFollowUpMessage ctx $"Set Whitelist stock to {amount}" else From bf68d0ae4abdfec12f05484f05b8bb8bbfdbc4df Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Wed, 27 Apr 2022 09:53:27 +0700 Subject: [PATCH 04/10] Refactor for new Item design --- Bot/Analytics.fs | 10 +-- Bot/GameHelpers.fs | 51 ++++++------ Bot/GameTypes.fs | 117 ++++++++++------------------ Bot/Games/Store.fs | 188 ++++++++++++++++++++++----------------------- 4 files changed, 162 insertions(+), 204 deletions(-) diff --git a/Bot/Analytics.fs b/Bot/Analytics.fs index ebd95de..58dd3a4 100644 --- a/Bot/Analytics.fs +++ b/Bot/Analytics.fs @@ -108,19 +108,19 @@ let sellWeaponCommand (discordMember : DiscordMember) weaponType = ] track "Sell Weapon Command Invoked" discordMember.Id data -let buyWeaponButton (discordMember : DiscordMember) (weapon : ItemDetails) = +let buyWeaponButton (discordMember : DiscordMember) itemName itemPrice = let data = [ "user_display_name" , discordMember.Username - "weapon_name" , weapon.Name - "weapon_price" , string weapon.Price + "weapon_name" , itemName + "weapon_price" , string itemPrice ] track "Buy Weapon Button Clicked" discordMember.Id data -let sellWeaponButton (discordMember : DiscordMember) (weapon : ItemDetails) = +let sellWeaponButton (discordMember : DiscordMember) (weapon : Item) price = let data = [ "user_display_name" , discordMember.Username "weapon_name" , weapon.Name - "weapon_price" , string weapon.Price + "weapon_price" , string price ] track "Sell Weapon Button Clicked" discordMember.Id data diff --git a/Bot/GameHelpers.fs b/Bot/GameHelpers.fs index cccf390..d99c006 100644 --- a/Bot/GameHelpers.fs +++ b/Bot/GameHelpers.fs @@ -7,39 +7,38 @@ open Degenz open Newtonsoft.Json module Armory = - let weapons : ItemDetails list = + let weapons : Inventory = let file = System.IO.File.ReadAllText("Items.json") // let file = System.IO.File.ReadAllText("Bot/Items.json") - JsonConvert.DeserializeObject(file) - |> Array.toList + JsonConvert.DeserializeObject(file) module Inventory = - let getItemsByType itemType inventory = - match itemType with - | ItemType.Hack -> inventory |> List.filter (fun item -> match item with Hack _ -> true | _ -> false) - | ItemType.Shield -> inventory |> List.filter (fun item -> match item with Shield _ -> true | _ -> false) - | ItemType.Food -> inventory |> List.filter (fun item -> match item with Food _ -> true | _ -> false) - | ItemType.Accessory -> inventory |> List.filter (fun item -> match item with Accessory _ -> true | _ -> false) +// let getItemsByType itemType inventory = +// match itemType with +// | ItemType.Hack -> inventory |> List.filter (fun item -> match item with Hack _ -> true | _ -> false) +// | ItemType.Shield -> inventory |> List.filter (fun item -> match item with Shield _ -> true | _ -> false) +// | ItemType.Food -> inventory |> List.filter (fun item -> match item with Food _ -> true | _ -> false) +// | ItemType.Accessory -> inventory |> List.filter (fun item -> match item with Accessory _ -> true | _ -> false) let findItemById id (inventory : Inventory) = inventory |> List.find (fun item -> item.Id = id) - let findHackById id inventory = - inventory |> List.pick (fun item -> match item with | Hack h -> (if h.Item.Id = id then Some h else None) | _ -> None) - let findShieldById id inventory = - inventory |> List.pick (fun item -> match item with | Shield s -> (if s.Item.Id = id then Some s else None) | _ -> None) - let findFoodById id inventory = - inventory |> List.pick (fun item -> match item with | Food f -> (if f.Item.Id = id then Some f else None) | _ -> None) - let findAccessoryById id inventory = - inventory |> List.pick (fun item -> match item with | Accessory a -> (if a.Item.Id = id then Some a else None) | _ -> None) - - let getHacks inventory = - inventory |> List.choose (fun item -> match item with | Hack h -> Some h | _ -> None) - let getShields inventory = - inventory |> List.choose (fun item -> match item with | Shield s -> Some s | _ -> None) - let getFoods inventory = - inventory |> List.choose (fun item -> match item with | Food f -> Some f | _ -> None) - let getAccessories inventory = - inventory |> List.choose (fun item -> match item with | Accessory a -> Some a | _ -> None) +// let findHackById id inventory = +// inventory |> List.pick (fun item -> match item with | Hack h -> (if h.Item.Id = id then Some h else None) | _ -> None) +// let findShieldById id inventory = +// inventory |> List.pick (fun item -> match item with | Shield s -> (if s.Item.Id = id then Some s else None) | _ -> None) +// let findFoodById id inventory = +// inventory |> List.pick (fun item -> match item with | Food f -> (if f.Item.Id = id then Some f else None) | _ -> None) +// let findAccessoryById id inventory = +// inventory |> List.pick (fun item -> match item with | Accessory a -> (if a.Item.Id = id then Some a else None) | _ -> None) +// +// let getHacks inventory = +// inventory |> List.choose (fun item -> match item with | Hack h -> Some h | _ -> None) +// let getShields inventory = +// inventory |> List.choose (fun item -> match item with | Shield s -> Some s | _ -> None) +// let getFoods inventory = +// inventory |> List.choose (fun item -> match item with | Food f -> Some f | _ -> None) +// let getAccessories inventory = +// inventory |> List.choose (fun item -> match item with | Accessory a -> Some a | _ -> None) module WeaponClass = let SameTargetAttackCooldown = TimeSpan.FromHours(4) diff --git a/Bot/GameTypes.fs b/Bot/GameTypes.fs index faece8d..1dc14b2 100644 --- a/Bot/GameTypes.fs +++ b/Bot/GameTypes.fs @@ -88,90 +88,52 @@ type ItemType = | Shield | Food | Accessory - -type ItemAttributes = { - CanBuy : bool - CanSell : bool - CanConsume : bool - CanTrade : bool - CanDrop : bool + +type Effect = + | Min of int + | Max of int + | Booster of int + | RateMultiplier of float + +type StatEffect = { + TargetStat : StatId + Effect : Effect } +type ItemAttribute = + | Buyable of price : int + | Sellable of price : int + | Expireable of lifetime : int + | Consumable of effects : StatEffect list + | Passive of effects : StatEffect list + | Droppable of chance : float + | Tradeable of yes : unit + | Attackable of power : int + | Defendable of resistance : int + | Classable of className : string + | Stackable of max : int + +let (|CanBuy|_|) itemAttrs = itemAttrs |> List.tryPick (function Buyable p -> Some p | _ -> None) +let (|CanSell|_|) itemAttrs = itemAttrs |> List.tryPick (function Sellable p -> Some p | _ -> None) +let (|CanExpire|_|) itemAttrs = itemAttrs |> List.tryPick (function Expireable l -> Some l | _ -> None) +let (|CanConsume|_|) itemAttrs = itemAttrs |> List.tryPick (function Consumable es -> Some es | _ -> None) +let (|CanPassive|_|) itemAttrs = itemAttrs |> List.tryPick (function Passive es -> Some es | _ -> None) +let (|CanDrop|_|) itemAttrs = itemAttrs |> List.tryPick (function Droppable c -> Some c | _ -> None) +let (|CanTrade|_|) itemAttrs = itemAttrs |> List.tryPick (function Tradeable () -> Some () | _ -> None) +let (|CanAttack|_|) itemAttrs = itemAttrs |> List.tryPick (function Attackable p -> Some p | _ -> None) +let (|CanDefend|_|) itemAttrs = itemAttrs |> List.tryPick (function Defendable r -> Some r | _ -> None) +let (|CanClass|_|) itemAttrs = itemAttrs |> List.tryPick (function Classable c -> Some c | _ -> None) +let (|CanStack|_|) itemAttrs = itemAttrs |> List.tryPick (function Stackable m -> Some m | _ -> None) + + type Item = { Id : int Name : string - Price : int - MaxAllowed : int - Attributes : ItemAttributes + Type : ItemType + Attributes : ItemAttribute list } -type HackItem = { - Power : int - Class : int - Cooldown : int - Item : Item -} - -type ShieldItem = { - Class : int - Cooldown : int - Item : Item -} - -type FoodItem = { - TargetStat : StatId - BoostAmount : int - Item : Item -} - -type AccessoryItem = { - TargetStat : StatId - FloorBoost : int - CeilBoost : int - Item : Item -} - -type MeleeWeapon = { - BreakChance : float - Item : Item -} - -type ItemDetails = - | Hack of HackItem - | Shield of ShieldItem - | Food of FoodItem - | Accessory of AccessoryItem - member this.Id = - match this with - | Hack i -> i.Item.Id - | Shield i -> i.Item.Id - | Food i -> i.Item.Id - | Accessory i -> i.Item.Id - member this.Name = - match this with - | Hack i -> i.Item.Name - | Shield i -> i.Item.Name - | Food i -> i.Item.Name - | Accessory i -> i.Item.Name - member this.Price = - match this with - | Hack i -> i.Item.Price - | Shield i -> i.Item.Price - | Food i -> i.Item.Price - | Accessory i -> i.Item.Price - member this.getItem = - match this with - | Hack i -> i.Item - | Shield i -> i.Item - | Food i -> i.Item - | Accessory i -> i.Item - static member getClass = function - | Hack i -> i.Class - | Shield i -> i.Class - | Food _ -> -1 - | Accessory _ -> -1 - -type Inventory = ItemDetails list +type Inventory = Item list type PlayerData = { DiscordId : uint64 @@ -195,3 +157,4 @@ with member this.toDiscordPlayer = { Id = this.DiscordId ; Name = this.Name } // XP = 0 Bank = 0 Active = false } + diff --git a/Bot/Games/Store.fs b/Bot/Games/Store.fs index 08b8081..a53a71b 100644 --- a/Bot/Games/Store.fs +++ b/Bot/Games/Store.fs @@ -15,30 +15,33 @@ let getBuyItemsEmbed (playerInventory : Inventory) (storeInventory : Inventory) storeInventory |> List.map (fun item -> let embed = DiscordEmbedBuilder() - match item with - | Hack hack -> - embed.AddField($"$GBT Reward |", string hack.Power, true) - .AddField("Cooldown |", $"{TimeSpan.FromMinutes(int hack.Cooldown).Minutes} minutes", true) - .WithThumbnail(Embeds.getItemIcon item.Id) - |> ignore - | Shield shield -> - embed.AddField($"Strong against |", WeaponClass.getGoodAgainst shield.Class |> snd |> string, true) -// .AddField($"Defensive Strength |", string item.Power, true) - .AddField("Active For |", $"{TimeSpan.FromMinutes(int shield.Cooldown).Hours} hours", true) - .WithThumbnail(Embeds.getItemIcon item.Id) - |> ignore - | Food food -> - embed.AddField($"Stat |", $"{food.TargetStat}", true) - .AddField($"Amount |", $"+{food.BoostAmount}", true) |> ignore - | Accessory accessory -> - embed.AddField($"Stat |", $"{accessory.TargetStat}", true) |> ignore - if accessory.FloorBoost > 0 then - embed.AddField($"Min Boost |", $"+{accessory.FloorBoost}", true) |> ignore - if accessory.CeilBoost > 0 then - embed.AddField($"Max Boost |", $"+{accessory.CeilBoost}", true) |> ignore + match item.Attributes with + | CanBuy price -> embed.AddField("Price πŸ’°", (if price = 0 then "Free" else $"{price} $GBT"), true) |> ignore + | CanAttack power -> + let title = match item.Type with ItemType.Hack -> "$GBT Reward" | _ -> "Power" + embed.AddField($"{title} |", string power, true) |> ignore + | CanExpire time -> + let title = match 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" + embed.AddField($"{title} |", timeStr, true) |> ignore + | CanConsume effects | CanPassive effects -> + let fx = + effects + |> List.map (fun f -> + match f.Effect with + | Min i -> $"Min - {f.TargetStat}" + | Max i -> $"Max - {f.TargetStat}" + | Booster i -> + let str = if i > 0 then "Boost" else "Penalty" + $"{str} - {f.TargetStat}" + | RateMultiplier i -> $"Multiplier - {f.TargetStat}") + |> String.concat "\n" + embed.AddField($"Effect - Amount |", $"{fx}", true) |> ignore + | _ -> () embed - .AddField("Price πŸ’°", (if item.Price = 0 then "Free" else $"{item.Price} $GBT"), true) .WithColor(WeaponClass.getClassEmbedColor item) + .WithThumbnail(Embeds.getItemIcon item.Id) .WithTitle($"{item.Name}") |> ignore let button = @@ -53,47 +56,36 @@ let getBuyItemsEmbed (playerInventory : Inventory) (storeInventory : Inventory) .AddComponents(buttons) .AsEphemeral(true) -let getSellEmbed (items : ItemDetails list) = +let getSellEmbed (items : Inventory) = let embeds , buttons = items - |> List.map (fun item -> - DiscordEmbedBuilder() - .AddField("Sell For πŸ’°", $"{item.Price} $GBT", true) - .WithTitle($"{item.Name}") - .WithColor(WeaponClass.getClassEmbedColor item) - .Build() - , DiscordButtonComponent(WeaponClass.getClassButtonColor item, $"Sell-{item.Id}", $"Sell {item.Name}") :> DiscordComponent) - |> List.unzip - - DiscordFollowupMessageBuilder() - .AddEmbeds(embeds) - .AddComponents(buttons) - .AsEphemeral(true) - -let getConsumeEmbed (items : ItemDetails list) = - let embeds , buttons = - items - |> List.groupBy (fun item -> item.Id) - |> List.map (fun (itemId , items ) -> - let item = List.head items - let foodItem = Inventory.findFoodById itemId items - DiscordEmbedBuilder() - .AddField($"{foodItem.Item.Name}", $"Total {items.Length}\nBoosts {foodItem.TargetStat} +{foodItem.BoostAmount}", true) - .WithTitle($"Food Items") - .WithColor(WeaponClass.getClassEmbedColor item) - .Build() - , DiscordButtonComponent(WeaponClass.getClassButtonColor item, $"Sell-{id}", $"Sell {item.Name}") :> DiscordComponent) + |> 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 checkHasSufficientFunds (item : Item) player = - if player.Bank - item.Price >= 0 - then Ok player - else Error $"You do not have sufficient funds to buy this item! Current balance: {player.Bank} GBT" + match item.Attributes with + | CanBuy price -> + if player.Bank - price >= 0 + 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 checkAlreadyOwnsItem (item : Item) player = if player.Inventory |> List.exists (fun w -> item.Id = w.Id) @@ -134,14 +126,15 @@ let handleBuyItem (ctx : IDiscordContext) itemId = executePlayerAction ctx (fun player -> async { let item = Armory.weapons |> Inventory.findItemById itemId do! player - |> checkHasSufficientFunds item.getItem - >>= checkAlreadyOwnsItem item.getItem + |> checkHasSufficientFunds item + >>= checkAlreadyOwnsItem item |> handleResultWithResponse ctx (fun player -> async { - let newBalance = player.Bank - item.Price + let price = match item.Attributes with CanBuy price -> price | _ -> 0 + let newBalance = player.Bank - price let p = { player with Bank = newBalance ; Inventory = item::player.Inventory } do! DbService.updatePlayer p |> Async.Ignore do! sendFollowUpMessage ctx $"Successfully purchased {item.Name}! You now have {newBalance} πŸ’°$GBT remaining" - do! Analytics.buyWeaponButton (ctx.GetDiscordMember()) item + do! Analytics.buyWeaponButton (ctx.GetDiscordMember()) item.Name price }) }) @@ -150,20 +143,23 @@ let handleSell (ctx : IDiscordContext) itemId = let item = Armory.weapons |> Inventory.findItemById itemId do! player - |> checkSoldItemAlready item.getItem + |> checkSoldItemAlready item |> handleResultWithResponse ctx (fun player -> async { - let updatedPlayer = { - player with - Bank = player.Bank + item.Price - Inventory = player.Inventory |> List.filter (fun i -> i.Id <> itemId) - } - do! - [ DbService.updatePlayer updatedPlayer |> Async.Ignore - DbService.removeShieldEvent updatedPlayer.DiscordId itemId |> Async.Ignore - sendFollowUpMessage ctx $"Sold {item.Name} for {item.Price}! Current Balance: {updatedPlayer.Bank}" - Analytics.sellWeaponButton (ctx.GetDiscordMember()) item ] - |> Async.Parallel - |> Async.Ignore + match item.Attributes with + | CanSell price -> + let updatedPlayer = { + player with + Bank = player.Bank + price + Inventory = player.Inventory |> List.filter (fun i -> i.Id <> itemId) + } + do! + [ DbService.updatePlayer updatedPlayer |> Async.Ignore + DbService.removeShieldEvent updatedPlayer.DiscordId itemId |> Async.Ignore + sendFollowUpMessage ctx $"Sold {item.Name} for {price}! Current Balance: {updatedPlayer.Bank}" + Analytics.sellWeaponButton (ctx.GetDiscordMember()) item price ] + |> Async.Parallel + |> Async.Ignore + | _ -> () }) }) @@ -187,33 +183,33 @@ let showInventoryEmbed (ctx : IDiscordContext) = PlayerInteractions.executePlaye player.Inventory |> List.map (fun item -> let embed = DiscordEmbedBuilder() - match item with - | Hack hack -> - embed.AddField($"$GBT Reward |", string hack.Power, true) - .AddField("Cooldown |", $"{TimeSpan.FromMinutes(int hack.Cooldown).Minutes} minutes", true) - .WithColor(DiscordColor.Red) - .WithThumbnail(Embeds.getItemIcon item.Id) - |> ignore - | Shield shield -> - embed.AddField($"Strong against |", WeaponClass.getGoodAgainst shield.Class |> snd |> string, true) -// .AddField($"Defensive Strength |", string item.Power, true) - .AddField("Active For |", $"{TimeSpan.FromMinutes(int shield.Cooldown).Hours} hours", true) - .WithColor(DiscordColor.SapGreen) - .WithThumbnail(Embeds.getItemIcon item.Id) - |> ignore - | Food food -> - embed.AddField($"Stat |", $"{food.TargetStat}", true) - .WithColor(DiscordColor.Azure) - .AddField($"Amount |", $"+{food.BoostAmount}", true) |> ignore - | Accessory accessory -> - embed.AddField($"Stat |", $"{accessory.TargetStat}", true) - .WithColor(DiscordColor.Goldenrod) |> ignore - if accessory.FloorBoost > 0 then - embed.AddField($"Min Boost |", $"+{accessory.FloorBoost}", true) |> ignore - if accessory.CeilBoost > 0 then - embed.AddField($"Max Boost |", $"+{accessory.CeilBoost}", true) |> ignore + match item.Attributes with + | CanBuy price -> embed.AddField("Price πŸ’°", (if price = 0 then "Free" else $"{price} $GBT"), true) |> ignore + | CanAttack power -> + let title = match item.Type with ItemType.Hack -> "$GBT Reward" | _ -> "Power" + embed.AddField($"{title} |", string power, true) |> ignore + | CanExpire time -> + let title = match 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" + embed.AddField($"{title} |", timeStr, true) |> ignore + | CanConsume effects | CanPassive effects -> + let fx = + effects + |> List.map (fun f -> + match f.Effect with + | Min i -> $"Min - {f.TargetStat}" + | Max i -> $"Max - {f.TargetStat}" + | Booster i -> + let str = if i > 0 then "Boost" else "Penalty" + $"{str} - {f.TargetStat}" + | RateMultiplier i -> $"Multiplier - {f.TargetStat}") + |> String.concat "\n" + embed.AddField($"Effect - Amount |", $"{fx}", true) |> ignore + | _ -> () embed - .AddField("Price πŸ’°", (if item.Price = 0 then "Free" else $"{item.Price} $GBT"), true) + .WithColor(WeaponClass.getClassEmbedColor item) + .WithThumbnail(Embeds.getItemIcon item.Id) .WithTitle($"{item.Name}") |> ignore let button = From f26d51701d1f1a1ba7c94d6a1bfc1ed1c703045f Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Thu, 28 Apr 2022 17:43:50 +0700 Subject: [PATCH 05/10] Add DB interactions for new item/store system. Improvements to attributes --- Bot/DbService.fs | 110 +++++++++++++++++--- Bot/Embeds.fs | 29 +++--- Bot/GameHelpers.fs | 104 +++++++++++-------- Bot/GameTypes.fs | 51 +++++++--- Bot/Games/HackerBattle.fs | 44 ++++---- Bot/Games/Store.fs | 204 ++++++++++++++++++-------------------- Bot/Games/Trainer.fs | 32 +++--- Bot/GuildEnvironment.fs | 1 + 8 files changed, 348 insertions(+), 227 deletions(-) diff --git a/Bot/DbService.fs b/Bot/DbService.fs index 27da3c9..7ae2e5c 100644 --- a/Bot/DbService.fs +++ b/Bot/DbService.fs @@ -1,11 +1,94 @@ ο»Ώmodule Degenz.DbService open System +open Npgsql open Npgsql.FSharp open Degenz let connStr = GuildEnvironment.connectionString +type StatMod = { mod_type :string ; target_stat : string ; mod_amount : float } +NpgsqlConnection.GlobalTypeMapper.MapComposite("stat_mod") |> ignore + +let readItem (reader : RowReader) = + let convertStatMod { mod_type = modType ; target_stat = targetStat; mod_amount = modAmount } = + let fx = + match modType with + | "Min" -> Min (int modAmount) + | "Max" -> Max (int modAmount) + | "RateMultiplier" -> RateMultiplier (modAmount) + | "Booster" -> Add (int modAmount) + | _ -> Add (int modAmount) + let ( _ , stat ) = StatId.TryParse(targetStat) + { TargetStat = stat ; Effect = fx } + { Item.Id = reader.int "id" + Item.Name = reader.string "name" + Item.Type = + match reader.string "category" with + | "Hack" -> ItemType.Hack + | "Shield" -> ItemType.Shield + | "Food" -> ItemType.Food + | "Accessory" -> ItemType.Accessory + | _ -> ItemType.Accessory + Item.Attributes = [ + reader.intOrNone "buy_price" |> Option.map (fun a -> Buyable (a * 1)) + reader.intOrNone "sell_price" |> Option.map (fun a -> Sellable (a * 1)) + reader.intOrNone "expiration" |> Option.map (fun a -> Expireable (a * 1)) + reader.floatOrNone "drop_chance" |> Option.map (float >> Droppable) + reader.intOrNone "attack_power" |> Option.map Attackable + reader.intOrNone "defense_power" |> Option.map Defendable + reader.stringOrNone "class_name" |> Option.map Classable + reader.intOrNone "max_stack" |> Option.map Stackable + if reader.bool "can_trade" then Some Tradeable else None + if reader.bool "can_consume" then Some Consumable else None + + (match reader.fieldValue "mods" with + | [||] -> None + | mods -> mods |> Array.map convertStatMod |> Array.toList |> Modifiable |> Some) + ] |> List.choose id +} + +let getPlayerInventory (did : uint64) = + connStr + |> Sql.connect + |> Sql.parameters [ "did", Sql.string (string did) ] + |> Sql.query """ + SELECT ii.id,name,category,buy_price,sell_price,rate_limit,expiration,drop_chance,can_trade,can_consume, + attack_power,defense_power,class_name,max_stack,mods + FROM inventory_item + JOIN item ii on inventory_item.item_id = ii.id + JOIN "user" usr on inventory_item.user_id = usr.id + WHERE usr.discord_id = @did; + """ + |> Sql.executeAsync readItem + |> Async.AwaitTask + +let addToPlayerInventory (did : uint64) (item : Item) = + connStr + |> Sql.connect + |> Sql.parameters [ ( "@did" , Sql.string (string did) ) ; ( "iid" , Sql.int item.Id )] + |> Sql.query """ + INSERT INTO inventory_item (item_id, user_id) + VALUES (@iid, (SELECT id FROM "user" WHERE discord_id = @did)); + """ + |> Sql.executeNonQueryAsync + |> Async.AwaitTask + +let getStoreItems (channelId : uint64) = + connStr + |> Sql.connect + |> Sql.parameters [ "cid", Sql.string (string channelId) ] + |> Sql.query """ + SELECT i.id,name,category,buy_price,sell_price,rate_limit,expiration,drop_chance,can_trade,can_consume, + attack_power,defense_power,class_name,max_stack,mods + FROM store_item + JOIN store st on store_item.store_id = st.id + JOIN item i on store_item.item_id = i.id + WHERE channel_id = @cid AND available; + """ + |> Sql.executeAsync readItem + |> Async.AwaitTask + let getPlayerEvents (did : uint64) = connStr |> Sql.connect @@ -59,28 +142,25 @@ let tryFindPlayer (discordId : uint64) = async { |> Sql.connect |> Sql.parameters [ "did", Sql.string (string discordId) ] |> Sql.query """ - SELECT discord_id, display_name, gbt, in_game, inventory, strength, focus, charisma, luck FROM "user" + SELECT discord_id, display_name, gbt, in_game, strength, focus, charisma, luck FROM "user" WHERE discord_id = @did """ |> Sql.executeAsync (fun read -> - let inv = read.intArray "inventory" - {| - DiscordId = read.string "discord_id" |> uint64 - Name = read.string "display_name" - Bank = read.intOrNone "gbt" |> Option.map ((*) 1) |> Option.defaultValue 0 - Inventory = inv |> Array.toList - Strength = read.intOrNone "strength" |> Option.defaultValue 0 - Focus = read.intOrNone "focus" |> Option.defaultValue 0 - Charisma = read.intOrNone "charisma" |> Option.defaultValue 0 - Luck = read.intOrNone "luck" |> Option.defaultValue 0 - Active = read.bool "in_game" + {| DiscordId = read.string "discord_id" |> uint64 + Name = read.string "display_name" + Bank = read.intOrNone "gbt" |> Option.map ((*) 1) |> Option.defaultValue 0 + Strength = read.intOrNone "strength" |> Option.defaultValue 0 + Focus = read.intOrNone "focus" |> Option.defaultValue 0 + Charisma = read.intOrNone "charisma" |> Option.defaultValue 0 + Luck = read.intOrNone "luck" |> Option.defaultValue 0 + Active = read.bool "in_game" |}) |> Async.AwaitTask match List.tryHead user with | None -> return None | Some u -> let! events = getPlayerEvents u.DiscordId - let inventory = u.Inventory |> List.choose (fun id -> Armory.weapons |> List.tryFind (fun item -> item.Id = id)) + let! inventory = getPlayerInventory discordId let strength = PlayerStats.calculateActiveStat StatId.Strength u.Strength inventory let focus = PlayerStats.calculateActiveStat StatId.Focus u.Focus inventory let charisma = PlayerStats.calculateActiveStat StatId.Charisma u.Charisma inventory @@ -116,14 +196,12 @@ let updatePlayer (player : PlayerData) = |> Sql.parameters [ "did", Sql.string (string player.DiscordId) "gbt", Sql.int (int player.Bank) - "inv", Sql.intArray (player.Inventory |> Array.ofList |> Array.map (fun item -> item.Id)) "strength", Sql.int player.Stats.Strength.Amount "focus", Sql.int player.Stats.Focus.Amount "charisma", Sql.int player.Stats.Charisma.Amount "luck", Sql.int player.Stats.Luck.Amount ] |> Sql.query """ - UPDATE "user" SET gbt = @gbt, inventory = @inv, - strength = @strength, focus = @focus, charisma = @charisma, luck = @luck + UPDATE "user" SET gbt = @gbt, strength = @strength, focus = @focus, charisma = @charisma, luck = @luck WHERE discord_id = @did """ |> Sql.executeNonQueryAsync diff --git a/Bot/Embeds.fs b/Bot/Embeds.fs index 6496a43..f17ff3d 100644 --- a/Bot/Embeds.fs +++ b/Bot/Embeds.fs @@ -1,6 +1,7 @@ module Degenz.Embeds open System +open Degenz open Degenz.Messaging open Degenz.Types open DSharpPlus.Entities @@ -10,13 +11,13 @@ let shieldGif = "https://s10.gifyu.com/images/Defense-Degenz-V2-min.gif" let getItemIcon id = match enum(id) with - | ItemId.Virus -> "https://s10.gifyu.com/images/Virus-icon.jpg" - | ItemId.RemoteAccess -> "https://s10.gifyu.com/images/Mind-Control-Degenz-V2-min.jpg" - | ItemId.Worm -> "https://s10.gifyu.com/images/WormBugAttack_Degenz-min.jpg" - | ItemId.Firewall -> "https://s10.gifyu.com/images/Defense-GIF-1-Degenz-1.jpg" - | ItemId.Encryption -> "https://s10.gifyu.com/images/Encryption-Degenz-V2-1-min.jpg" - | ItemId.Cypher -> "https://s10.gifyu.com/images/Cypher-Smaller.jpg" - | _ -> hackGif + | ItemId.Virus -> Some "https://s10.gifyu.com/images/Virus-icon.jpg" + | ItemId.RemoteAccess -> Some "https://s10.gifyu.com/images/Mind-Control-Degenz-V2-min.jpg" + | ItemId.Worm -> Some "https://s10.gifyu.com/images/WormBugAttack_Degenz-min.jpg" + | ItemId.Firewall -> Some "https://s10.gifyu.com/images/Defense-GIF-1-Degenz-1.jpg" + | ItemId.Encryption -> Some "https://s10.gifyu.com/images/Encryption-Degenz-V2-1-min.jpg" + | ItemId.Cypher -> Some "https://s10.gifyu.com/images/Cypher-Smaller.jpg" + | _ -> None let getItemGif id = match enum(id) with @@ -61,8 +62,8 @@ let pickDefense actionId player isTrainer = for shield in Inventory.getShields player.Inventory do let hours = TimeSpan.FromMinutes(int shield.Cooldown).TotalHours |> int - let against = WeaponClass.getGoodAgainst(shield.Class) |> snd - embed.AddField(shield.Item.Name, $"Active {hours} hours\nDefeats {against}", true) |> ignore + let against = WeaponClass.getGoodAgainst shield.Class |> snd + embed.AddField(shield.Name, $"Active {hours} hours\nDefeats {against}", true) |> ignore DiscordFollowupMessageBuilder() .AddComponents(buttons) @@ -85,7 +86,7 @@ let pickHack actionId attacker defender isTrainer = if not isTrainer then for hack in Inventory.getHacks attacker.Inventory do let amount = if hack.Power > int defender.Bank then int defender.Bank else hack.Power - embed.AddField(hack.Item.Name, $"Cooldown {hack.Cooldown} mins\nExtract {amount} $GBT", true) |> ignore + embed.AddField(hack.Name, $"Cooldown {hack.Cooldown} mins\nExtract {amount} $GBT", true) |> ignore DiscordFollowupMessageBuilder() .AddComponents(buttons) @@ -95,9 +96,9 @@ let pickHack actionId attacker defender isTrainer = let responseSuccessfulHack earnedMoney (targetId : uint64) amountTaken (hack : HackItem) = let embed = DiscordEmbedBuilder() - .WithImageUrl(getItemGif hack.Item.Id) + .WithImageUrl(getItemGif hack.Id) .WithTitle("Hack Attack") - .WithDescription($"You successfully hacked <@{targetId}> using {hack.Item.Name}" + .WithDescription($"You successfully hacked <@{targetId}> using {hack.Name}" + (if earnedMoney then $", and took {amountTaken} πŸ’°$GBT from them!" else "!")) DiscordFollowupMessageBuilder() @@ -105,9 +106,9 @@ let responseSuccessfulHack earnedMoney (targetId : uint64) amountTaken (hack : H .AsEphemeral(true) let responseCreatedShield (shield : ShieldItem) = - let embed = DiscordEmbedBuilder().WithImageUrl(getItemGif shield.Item.Id) + let embed = DiscordEmbedBuilder().WithImageUrl(getItemGif shield.Id) embed.Title <- "Mounted Shield" - embed.Description <- $"Mounted {shield.Item.Name} shield for {TimeSpan.FromMinutes(int shield.Cooldown).TotalHours} hours" + embed.Description <- $"Mounted {shield.Name} shield for {TimeSpan.FromMinutes(int shield.Cooldown).TotalHours} hours" DiscordFollowupMessageBuilder() .AddEmbed(embed) diff --git a/Bot/GameHelpers.fs b/Bot/GameHelpers.fs index d99c006..5a0dc5f 100644 --- a/Bot/GameHelpers.fs +++ b/Bot/GameHelpers.fs @@ -13,15 +13,37 @@ module Armory = JsonConvert.DeserializeObject(file) module Inventory = -// let getItemsByType itemType inventory = -// match itemType with -// | ItemType.Hack -> inventory |> List.filter (fun item -> match item with Hack _ -> true | _ -> false) -// | ItemType.Shield -> inventory |> List.filter (fun item -> match item with Shield _ -> true | _ -> false) -// | ItemType.Food -> inventory |> List.filter (fun item -> match item with Food _ -> true | _ -> false) -// | ItemType.Accessory -> inventory |> List.filter (fun item -> match item with Accessory _ -> true | _ -> false) + let getItemsByType itemType inventory = + match itemType with + | ItemType.Hack -> inventory |> List.filter (fun item -> match item.Type with ItemType.Hack _ -> true | _ -> false) + | ItemType.Shield -> inventory |> List.filter (fun item -> match item.Type with ItemType.Shield _ -> true | _ -> false) + | ItemType.Food -> inventory |> List.filter (fun item -> match item.Type with ItemType.Food _ -> true | _ -> false) + | ItemType.Accessory -> inventory |> List.filter (fun item -> match item.Type with ItemType.Accessory _ -> true | _ -> false) let findItemById id (inventory : Inventory) = inventory |> List.find (fun item -> item.Id = id) + let getHackItem item = + match item.Type , item.Attributes with + | ItemType.Hack , CanBuy buyPrice & CanSell _ & CanAttack power & CanExpire cooldown & CanClass ``class``-> + Some { Id = item.Id + Name = item.Name + Price = buyPrice + Cooldown = cooldown + Power = power + Class = ``class`` } + | _ -> None + + let getShieldItem item = + match item.Type , item.Attributes with + | ItemType.Shield , CanBuy buyPrice & CanSell _ & CanDefend resistance & CanExpire cooldown & CanClass ``class`` -> + Some { Id = item.Id + Name = item.Name + Price = buyPrice + Cooldown = cooldown + Resistance = resistance + Class = ``class`` } + | _ -> None + // let findHackById id inventory = // inventory |> List.pick (fun item -> match item with | Hack h -> (if h.Item.Id = id then Some h else None) | _ -> None) // let findShieldById id inventory = @@ -31,36 +53,34 @@ module Inventory = // let findAccessoryById id inventory = // inventory |> List.pick (fun item -> match item with | Accessory a -> (if a.Item.Id = id then Some a else None) | _ -> None) // -// let getHacks inventory = -// inventory |> List.choose (fun item -> match item with | Hack h -> Some h | _ -> None) -// let getShields inventory = -// inventory |> List.choose (fun item -> match item with | Shield s -> Some s | _ -> None) -// let getFoods inventory = -// inventory |> List.choose (fun item -> match item with | Food f -> Some f | _ -> None) -// let getAccessories inventory = -// inventory |> List.choose (fun item -> match item with | Accessory a -> Some a | _ -> None) + let getHacks inventory = inventory |> List.choose getHackItem + let getShields inventory = inventory |> List.choose getShieldItem + let getFoods inventory = + inventory |> List.choose (fun item -> match item.Type with | ItemType.Food -> Some item | _ -> None) + let getAccessories inventory = + inventory |> List.choose (fun item -> match item.Type with | ItemType.Accessory -> Some item | _ -> None) module WeaponClass = let SameTargetAttackCooldown = TimeSpan.FromHours(4) let getClassButtonColor item = - match ItemDetails.getClass item with - | 0 -> ButtonStyle.Danger - | 1 -> ButtonStyle.Primary - | 2 -> ButtonStyle.Success - | _ -> ButtonStyle.Primary + match item.Attributes with + | CanClass "0" -> ButtonStyle.Danger + | CanClass "1" -> ButtonStyle.Primary + | CanClass "2" -> ButtonStyle.Success + | _ -> ButtonStyle.Primary let getClassEmbedColor item = - match ItemDetails.getClass item with - | 0 -> DiscordColor.Red - | 1 -> DiscordColor.Blurple - | 2 -> DiscordColor.Green - | _ -> DiscordColor.Blurple + match item.Attributes with + | CanClass "0" -> DiscordColor.Red + | CanClass "1" -> DiscordColor.Blurple + | CanClass "2" -> DiscordColor.Green + | _ -> DiscordColor.Blurple let getGoodAgainst = function - | 0 -> ( ItemId.Firewall , ItemId.Virus ) - | 1 -> ( ItemId.Encryption , ItemId.RemoteAccess ) - | _ -> ( ItemId.Cypher , ItemId.Worm ) + | "0" -> ( ItemId.Firewall , ItemId.Virus ) + | "1" -> ( ItemId.Encryption , ItemId.RemoteAccess ) + | _ -> ( ItemId.Cypher , ItemId.Worm ) module Player = let getHackEvents player = @@ -81,11 +101,11 @@ module Player = let modifyBank (player : PlayerData) amount = { player with Bank = max (player.Bank + amount) 0 } module PlayerStats = - // 4.17f would go from 100 to 0 in roughly 24 hours - let Strength = { Id = StatId.Strength ; BaseDecayRate = 4.17 ; BaseRange = Range.normalized } - let Focus = { Id = StatId.Focus ; BaseDecayRate = 4.17 ; BaseRange = Range.normalized } - let Luck = { Id = StatId.Luck ; BaseDecayRate = 4.17 ; BaseRange = Range.normalized } - let Charisma = { Id = StatId.Charisma ; BaseDecayRate = 4.17 ; BaseRange = Range.normalized } + // 2.09f would go from 100 to 0 in roughly 48 hours + let Strength = { Id = StatId.Strength ; BaseDecayRate = 2.09 ; BaseRange = Range.normalized } + let Focus = { Id = StatId.Focus ; BaseDecayRate = 2.09 ; BaseRange = Range.normalized } + let Luck = { Id = StatId.Luck ; BaseDecayRate = 2.09 ; BaseRange = Range.normalized } + let Charisma = { Id = StatId.Charisma ; BaseDecayRate = 2.09 ; BaseRange = Range.normalized } let stats = [ Strength ; Focus ; Charisma ; Luck ] @@ -104,18 +124,20 @@ module PlayerStats = let modMinMax = let min = items - |> List.filter (fun item -> match item with | Accessory a -> a.TargetStat = statId | _ -> false) - |> List.sumBy (fun item -> match item with | Accessory a -> a.FloorBoost | _ -> 0) + |> List.choose (fun i -> match i.Attributes with CanModify fx -> Some fx | _ -> None) + |> List.concat + |> List.sumBy (fun fx -> match fx.Effect with | Min x -> x | _ -> 0) let max = items - |> List.filter (fun item -> match item with | Accessory a -> a.TargetStat = statId | _ -> false) - |> List.sumBy (fun item -> match item with | Accessory a -> a.CeilBoost | _ -> 0) + |> List.choose (fun i -> match i.Attributes with CanModify fx -> Some fx | _ -> None) + |> List.concat + |> List.sumBy (fun fx -> match fx.Effect with | Max x -> x | _ -> 0) Range.create (statConfig.BaseRange.Min + min) (statConfig.BaseRange.Max + max) let amountAfterDecay = modMinMax |> Range.constrain amount { Id = statId ; Amount = amountAfterDecay ; ModRange = modMinMax ; LastRead = DateTime.UtcNow } module Arsenal = - let battleItemFormat (items : ItemDetails list) = + let battleItemFormat (items : Inventory) = match items with | [] -> "None" | _ -> items |> List.map (fun item -> item.Name) |> String.concat ", " @@ -128,13 +150,13 @@ module Arsenal = |> List.map (fun event -> match event.Type with | Hacking h -> - let item = Armory.weapons |> Inventory.findHackById h.HackId + let item = Armory.weapons |> Inventory.findItemById h.HackId let cooldown = Messaging.getTimeText false WeaponClass.SameTargetAttackCooldown event.Timestamp - $"Hacked {h.Adversary.Name} with {item.Item.Name} {cooldown} ago" + $"Hacked {h.Adversary.Name} with {item.Name} {cooldown} ago" | Shielding id -> - let item = Armory.weapons |> Inventory.findShieldById id + let item = Armory.weapons |> Inventory.findItemById id let cooldown = Messaging.getTimeText true (TimeSpan.FromMinutes(int event.Cooldown)) event.Timestamp - $"{item.Item.Name} Shield active for {cooldown}" + $"{item.Name} Shield active for {cooldown}" | _ -> "") |> List.filter (String.IsNullOrWhiteSpace >> not) |> String.concat "\n" diff --git a/Bot/GameTypes.fs b/Bot/GameTypes.fs index 1dc14b2..ddf003f 100644 --- a/Bot/GameTypes.fs +++ b/Bot/GameTypes.fs @@ -92,7 +92,7 @@ type ItemType = type Effect = | Min of int | Max of int - | Booster of int + | Add of int | RateMultiplier of float type StatEffect = { @@ -103,29 +103,30 @@ type StatEffect = { type ItemAttribute = | Buyable of price : int | Sellable of price : int + | RateLimitable of cooldown : int | Expireable of lifetime : int - | Consumable of effects : StatEffect list - | Passive of effects : StatEffect list + | Consumable + | Modifiable of effects : StatEffect list | Droppable of chance : float - | Tradeable of yes : unit + | Tradeable | Attackable of power : int | Defendable of resistance : int | Classable of className : string | Stackable of max : int -let (|CanBuy|_|) itemAttrs = itemAttrs |> List.tryPick (function Buyable p -> Some p | _ -> None) -let (|CanSell|_|) itemAttrs = itemAttrs |> List.tryPick (function Sellable p -> Some p | _ -> None) -let (|CanExpire|_|) itemAttrs = itemAttrs |> List.tryPick (function Expireable l -> Some l | _ -> None) -let (|CanConsume|_|) itemAttrs = itemAttrs |> List.tryPick (function Consumable es -> Some es | _ -> None) -let (|CanPassive|_|) itemAttrs = itemAttrs |> List.tryPick (function Passive es -> Some es | _ -> None) -let (|CanDrop|_|) itemAttrs = itemAttrs |> List.tryPick (function Droppable c -> Some c | _ -> None) -let (|CanTrade|_|) itemAttrs = itemAttrs |> List.tryPick (function Tradeable () -> Some () | _ -> None) -let (|CanAttack|_|) itemAttrs = itemAttrs |> List.tryPick (function Attackable p -> Some p | _ -> None) -let (|CanDefend|_|) itemAttrs = itemAttrs |> List.tryPick (function Defendable r -> Some r | _ -> None) -let (|CanClass|_|) itemAttrs = itemAttrs |> List.tryPick (function Classable c -> Some c | _ -> None) -let (|CanStack|_|) itemAttrs = itemAttrs |> List.tryPick (function Stackable m -> Some m | _ -> None) +let (|CanBuy|_|) itemAttrs = itemAttrs |> List.tryPick (function Buyable p -> Some p | _ -> None) +let (|CanSell|_|) itemAttrs = itemAttrs |> List.tryPick (function Sellable p -> Some p | _ -> None) +let (|CanExpire|_|) itemAttrs = itemAttrs |> List.tryPick (function Expireable l -> Some l | _ -> None) +let (|CanRateLimit|_|) itemAttrs = itemAttrs |> List.tryPick (function RateLimitable l -> Some l | _ -> None) +let (|CanConsume|_|) itemAttrs = itemAttrs |> List.tryPick (function Consumable -> Some () | _ -> None) +let (|CanModify|_|) itemAttrs = itemAttrs |> List.tryPick (function Modifiable es -> Some es | _ -> None) +let (|CanDrop|_|) itemAttrs = itemAttrs |> List.tryPick (function Droppable c -> Some c | _ -> None) +let (|CanTrade|_|) itemAttrs = itemAttrs |> List.tryPick (function Tradeable -> Some () | _ -> None) +let (|CanAttack|_|) itemAttrs = itemAttrs |> List.tryPick (function Attackable p -> Some p | _ -> None) +let (|CanDefend|_|) itemAttrs = itemAttrs |> List.tryPick (function Defendable r -> Some r | _ -> None) +let (|CanClass|_|) itemAttrs = itemAttrs |> List.tryPick (function Classable c -> Some c | _ -> None) +let (|CanStack|_|) itemAttrs = itemAttrs |> List.tryPick (function Stackable m -> Some m | _ -> None) - type Item = { Id : int Name : string @@ -133,6 +134,24 @@ type Item = { Attributes : ItemAttribute list } +type HackItem = { + Id : int + Name : string + Price : int + Cooldown : int + Power : int + Class : string +} + +type ShieldItem = { + Id : int + Name : string + Price : int + Cooldown : int + Resistance : int + Class : string +} + type Inventory = Item list type PlayerData = { diff --git a/Bot/Games/HackerBattle.fs b/Bot/Games/HackerBattle.fs index 18745bf..9a3b8b3 100644 --- a/Bot/Games/HackerBattle.fs +++ b/Bot/Games/HackerBattle.fs @@ -52,7 +52,7 @@ let checkHasEmptyHacks (attacker : PlayerData) = let checkPlayerOwnsWeapon (item : Item) player = match player.Inventory |> List.exists (fun i -> i.Id = item.Id) with | true -> Ok player - | false -> Error $"You sold your weapon already, you cheeky bastard..." + | false -> Error $"You sold your {item.Name} already, you cheeky bastard..." let checkPlayerHasShieldSlotsAvailable player = let updatedPlayer = player |> Player.removeExpiredActions @@ -82,7 +82,11 @@ let runHackerBattle defender (hack : HackItem) = |> fun p -> p.Events |> List.exists (fun event -> match event.Type with - | Shielding id -> hack.Class = (Inventory.findShieldById id Armory.weapons).Class + | Shielding id -> + let item = Inventory.findItemById id Armory.weapons + match item.Attributes with + | CanClass c -> hack.Class = c + | _ -> false | _ -> false) let updateCombatants successfulHack (attacker : PlayerData) (defender : PlayerData) (hack : HackItem) prize = @@ -90,7 +94,7 @@ let updateCombatants successfulHack (attacker : PlayerData) (defender : PlayerDa { p with Events = attack::p.Events ; Bank = max (p.Bank + amount) 0 } let event isDefenderEvent = let hackEvent = { - HackId = hack.Item.Id + HackId = hack.Id Adversary = if isDefenderEvent then attacker.toDiscordPlayer else defender.toDiscordPlayer IsInstigator = not isDefenderEvent Success = successfulHack @@ -161,25 +165,26 @@ let handleAttack (ctx : IDiscordContext) = executePlayerAction ctx (fun attacker -> async { let tokens = ctx.GetInteractionId().Split("-") let hackId = int tokens.[1] - let hack = Armory.weapons |> Inventory.findHackById hackId + let item = Armory.weapons |> Inventory.findItemById hackId + let hackItem = (Inventory.getHackItem item).Value let resultId , targetId = UInt64.TryParse tokens.[2] let! resultTarget = DbService.tryFindPlayer targetId - match resultTarget , true , resultId with - | Some defender , true , true -> + match resultTarget , resultId with + | Some defender , true -> do! attacker |> Player.removeExpiredActions |> checkAlreadyHackedTarget defender - >>= checkPlayerOwnsWeapon hack.Item + >>= checkPlayerOwnsWeapon item >>= checkTargetHasFunds defender - >>= checkWeaponHasCooldown hack.Item + >>= checkWeaponHasCooldown item |> function - | Ok atkr -> async { - let result = runHackerBattle defender hack + | Ok attacker -> async { + let result = runHackerBattle defender hackItem match result with - | false -> do! successfulHack ctx atkr defender hack - | true -> do! failedHack ctx attacker defender hack - do! Analytics.hackedTarget (ctx.GetDiscordMember()) hack.Item.Name (not result) + | false -> do! successfulHack ctx attacker defender hackItem + | true -> do! failedHack ctx attacker defender hackItem + do! Analytics.hackedTarget (ctx.GetDiscordMember()) hackItem.Name (not result) } | Error msg -> Messaging.sendFollowUpMessage ctx msg | _ -> do! Messaging.sendFollowUpMessage ctx "Error occurred processing attack" @@ -201,18 +206,19 @@ let handleDefense (ctx : IDiscordContext) = executePlayerAction ctx (fun player -> async { let tokens = ctx.GetInteractionId().Split("-") let shieldId = int tokens.[1] - let shield = Armory.weapons |> Inventory.findShieldById shieldId + let item = Armory.weapons |> Inventory.findItemById shieldId + let shieldItem = (Inventory.getShieldItem item).Value do! player - |> checkPlayerOwnsWeapon shield.Item + |> checkPlayerOwnsWeapon item >>= checkPlayerHasShieldSlotsAvailable - >>= checkWeaponHasCooldown shield.Item + >>= checkWeaponHasCooldown item |> handleResultWithResponse ctx (fun p -> async { - let embed = Embeds.responseCreatedShield shield + let embed = Embeds.responseCreatedShield shieldItem do! ctx.FollowUp embed |> Async.AwaitTask let defense = { Type = Shielding shieldId - Cooldown = shield.Cooldown + Cooldown = shieldItem.Cooldown Timestamp = DateTime.UtcNow } do! DbService.updatePlayer p |> Async.Ignore @@ -223,7 +229,7 @@ let handleDefense (ctx : IDiscordContext) = do! channel.SendMessageAsync(builder) |> Async.AwaitTask |> Async.Ignore - do! Analytics.shieldActivated (ctx.GetDiscordMember()) shield.Item.Name + do! Analytics.shieldActivated (ctx.GetDiscordMember()) shieldItem.Name }) }) diff --git a/Bot/Games/Store.fs b/Bot/Games/Store.fs index a53a71b..452a187 100644 --- a/Bot/Games/Store.fs +++ b/Bot/Games/Store.fs @@ -10,46 +10,61 @@ open Degenz open Degenz.Messaging open Degenz.PlayerInteractions -let getBuyItemsEmbed (playerInventory : Inventory) (storeInventory : Inventory) = - let embeds , buttons = - storeInventory - |> List.map (fun item -> - let embed = DiscordEmbedBuilder() - match item.Attributes with - | CanBuy price -> embed.AddField("Price πŸ’°", (if price = 0 then "Free" else $"{price} $GBT"), true) |> ignore - | CanAttack power -> +let getItemEmbeds owned (items : Inventory) = + items + |> List.countBy (fun item -> item.Id) + |> List.map (fun (id,count) -> items |> List.find (fun i -> i.Id = id) , count ) + |> List.map (fun (item,count) -> + let embed = DiscordEmbedBuilder() + item.Attributes + |> List.iter (function + | Buyable price -> embed.AddField("Price πŸ’°", (if price = 0 then "Free" else $"{price} $GBT"), true) |> ignore + | Attackable power -> let title = match item.Type with ItemType.Hack -> "$GBT Reward" | _ -> "Power" embed.AddField($"{title} |", string power, true) |> ignore - | CanExpire time -> + | RateLimitable time -> let title = match 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" embed.AddField($"{title} |", timeStr, true) |> ignore - | CanConsume effects | CanPassive effects -> + | Stackable max -> + if owned then + embed.AddField($"Total Owned |", $"{count}", true) |> ignore + else + embed.AddField($"Max Allowed |", $"{max}", true) |> ignore + | Modifiable effects -> let fx = effects |> List.map (fun f -> match f.Effect with - | Min i -> $"Min - {f.TargetStat}" - | Max i -> $"Max - {f.TargetStat}" - | Booster i -> + | Min i -> $"{f.TargetStat} Min + {i}" + | Max i -> $"{f.TargetStat} Max + {i}" + | Add i -> let str = if i > 0 then "Boost" else "Penalty" - $"{str} - {f.TargetStat}" - | RateMultiplier i -> $"Multiplier - {f.TargetStat}") + $"{f.TargetStat} {str} + {i}" + | RateMultiplier i -> $"{f.TargetStat} Multiplier - i") |> String.concat "\n" - embed.AddField($"Effect - Amount |", $"{fx}", true) |> ignore - | _ -> () - embed - .WithColor(WeaponClass.getClassEmbedColor item) - .WithThumbnail(Embeds.getItemIcon item.Id) - .WithTitle($"{item.Name}") - |> ignore - let button = - if playerInventory |> List.exists (fun i -> i.Id = item.Id) - then DiscordButtonComponent(WeaponClass.getClassButtonColor item, $"Buy-{item.Id}", $"Own {item.Name}", true) - else DiscordButtonComponent(WeaponClass.getClassButtonColor item, $"Buy-{item.Id}", $"Buy {item.Name}") - ( embed.Build() , button :> DiscordComponent )) - |> List.unzip + embed.AddField($"Effect - Amount ", $"{fx}", true) |> ignore + | _ -> ()) + embed + .WithColor(WeaponClass.getClassEmbedColor item) + .WithTitle($"{item.Name}") + |> ignore + match Embeds.getItemIcon item.Id with + | Some url -> embed.WithThumbnail(url) + | None -> embed) + |> List.map (fun e -> e.Build()) + |> Seq.ofList + +let getBuyItemsEmbed (playerInventory : Inventory) (storeInventory : Inventory) = + let embeds = getItemEmbeds false storeInventory + let buttons = + storeInventory + |> List.map (fun item -> + if playerInventory |> List.exists (fun i -> i.Id = item.Id) + then DiscordButtonComponent(WeaponClass.getClassButtonColor item, $"Buy-{item.Id}", $"Own {item.Name}", true) + else DiscordButtonComponent(WeaponClass.getClassButtonColor item, $"Buy-{item.Id}", $"Buy {item.Name}") + :> DiscordComponent) DiscordFollowupMessageBuilder() .AddEmbeds(embeds) @@ -87,12 +102,21 @@ let checkHasSufficientFunds (item : Item) 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 checkAlreadyOwnsItem (item : Item) player = - if player.Inventory |> List.exists (fun w -> item.Id = w.Id) - then Error $"You already own {item.Name}!" - else Ok player +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 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." @@ -105,10 +129,15 @@ let checkHasItemsInArsenal itemType items player = let buy itemType (ctx : IDiscordContext) = executePlayerAction ctx (fun player -> async { let playerItems = Inventory.getItemsByType itemType player.Inventory - let armoryItems = Inventory.getItemsByType itemType Armory.weapons - let itemStore = getBuyItemsEmbed playerItems armoryItems - do! ctx.FollowUp itemStore |> Async.AwaitTask - do! Analytics.buyWeaponCommand (ctx.GetDiscordMember()) itemType + try + let! armoryItems = DbService.getStoreItems (ctx.GetChannel().Id) + if armoryItems.Length > 0 then + let itemStore = getBuyItemsEmbed playerItems armoryItems + do! ctx.FollowUp itemStore |> Async.AwaitTask + do! Analytics.buyWeaponCommand (ctx.GetDiscordMember()) itemType + else + do! Messaging.sendFollowUpMessage ctx "This channel doesn't have anything to sell" + with ex -> printfn $"{ex.Message}" }) let sell itemType getItems (ctx : IDiscordContext) = @@ -124,16 +153,16 @@ let sell itemType getItems (ctx : IDiscordContext) = // TODO: When you buy a shield, prompt the user to activate it let handleBuyItem (ctx : IDiscordContext) itemId = executePlayerAction ctx (fun player -> async { - let item = Armory.weapons |> Inventory.findItemById itemId + let! storeInventory = DbService.getStoreItems (ctx.GetChannel().Id) + let item = storeInventory |> Inventory.findItemById itemId do! player |> checkHasSufficientFunds item - >>= checkAlreadyOwnsItem item + >>= checkDoesntExceedStackCap item |> handleResultWithResponse ctx (fun player -> async { let price = match item.Attributes with CanBuy price -> price | _ -> 0 - let newBalance = player.Bank - price - let p = { player with Bank = newBalance ; Inventory = item::player.Inventory } - do! DbService.updatePlayer p |> Async.Ignore - do! sendFollowUpMessage ctx $"Successfully purchased {item.Name}! You now have {newBalance} πŸ’°$GBT remaining" + do! DbService.updatePlayerCurrency -price player |> Async.Ignore + do! DbService.addToPlayerInventory player.DiscordId item |> Async.Ignore + do! sendFollowUpMessage ctx $"Successfully purchased {item.Name}! You now have {player.Bank - price} πŸ’°$GBT remaining" do! Analytics.buyWeaponButton (ctx.GetDiscordMember()) item.Name price }) }) @@ -163,68 +192,18 @@ let handleSell (ctx : IDiscordContext) itemId = }) }) -let handleStoreEvents (_ : DiscordClient) (event : ComponentInteractionCreateEventArgs) = - let ctx = DiscordEventContext event :> IDiscordContext - let id = ctx.GetInteractionId() - let itemId = int <| id.Split("-").[1] - match id with - | id when id.StartsWith("Buy") -> handleBuyItem ctx itemId - | id when id.StartsWith("Sell") -> handleSell ctx itemId - | _ -> - task { - let builder = DiscordInteractionResponseBuilder() - builder.IsEphemeral <- true - builder.Content <- $"Incorrect Action identifier {id}" - do! ctx.Respond(InteractionResponseType.ChannelMessageWithSource, builder) |> Async.AwaitTask - } +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 -> + let embeds = getItemEmbeds true items + let builder = DiscordFollowupMessageBuilder().AddEmbeds(embeds).AsEphemeral(true) + do! ctx.FollowUp builder |> Async.AwaitTask +}) let showInventoryEmbed (ctx : IDiscordContext) = PlayerInteractions.executePlayerAction ctx (fun player -> async { - let embeds , buttons = - player.Inventory - |> List.map (fun item -> - let embed = DiscordEmbedBuilder() - match item.Attributes with - | CanBuy price -> embed.AddField("Price πŸ’°", (if price = 0 then "Free" else $"{price} $GBT"), true) |> ignore - | CanAttack power -> - let title = match item.Type with ItemType.Hack -> "$GBT Reward" | _ -> "Power" - embed.AddField($"{title} |", string power, true) |> ignore - | CanExpire time -> - let title = match 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" - embed.AddField($"{title} |", timeStr, true) |> ignore - | CanConsume effects | CanPassive effects -> - let fx = - effects - |> List.map (fun f -> - match f.Effect with - | Min i -> $"Min - {f.TargetStat}" - | Max i -> $"Max - {f.TargetStat}" - | Booster i -> - let str = if i > 0 then "Boost" else "Penalty" - $"{str} - {f.TargetStat}" - | RateMultiplier i -> $"Multiplier - {f.TargetStat}") - |> String.concat "\n" - embed.AddField($"Effect - Amount |", $"{fx}", true) |> ignore - | _ -> () - embed - .WithColor(WeaponClass.getClassEmbedColor item) - .WithThumbnail(Embeds.getItemIcon item.Id) - .WithTitle($"{item.Name}") - |> ignore - let button = -// if playerInventory |> List.exists (fun i -> i.Id = item.Id) -// then DiscordButtonComponent(WeaponClass.getClassButtonColor item, $"Buy-{item.Id}", $"Own {item.Name}", true) -// else DiscordButtonComponent(WeaponClass.getClassButtonColor item, $"Buy-{item.Id}", $"Buy {item.Name}") - DiscordButtonComponent(WeaponClass.getClassButtonColor item, $"Sell-{item.Id}", $"Sell {item.Name}") - ( embed.Build() , button :> DiscordComponent )) - |> List.unzip - - let builder = - DiscordFollowupMessageBuilder() - .AddEmbeds(embeds) -// .AddComponents(buttons) - .AsEphemeral(true) + let embeds = getItemEmbeds true player.Inventory + let builder = DiscordFollowupMessageBuilder().AddEmbeds(embeds).AsEphemeral(true) do! ctx.FollowUp builder |> Async.AwaitTask }) @@ -233,7 +212,6 @@ let showStats (ctx : IDiscordContext) = PlayerInteractions.executePlayerAction c PlayerStats.stats |> List.iter (fun statConfig -> let playerStat = PlayerStats.getPlayerStat statConfig player -// let diffSymbol lhs rhs = if lhs < rhs then "-" elif lhs = rhs then "" else "+" let min = match statConfig.BaseRange.Min = playerStat.ModRange.Min with | true -> $"{statConfig.BaseRange.Min}" @@ -251,6 +229,21 @@ let showStats (ctx : IDiscordContext) = PlayerInteractions.executePlayerAction c do! ctx.FollowUp builder |> Async.AwaitTask }) +let handleStoreEvents (_ : DiscordClient) (event : ComponentInteractionCreateEventArgs) = + let ctx = DiscordEventContext event :> IDiscordContext + let id = ctx.GetInteractionId() + let itemId = int <| id.Split("-").[1] + match id with + | id when id.StartsWith("Buy") -> handleBuyItem ctx itemId + | id when id.StartsWith("Sell") -> handleSell ctx itemId + | _ -> + task { + let builder = DiscordInteractionResponseBuilder() + builder.IsEphemeral <- true + builder.Content <- $"Incorrect Action identifier {id}" + do! ctx.Respond(InteractionResponseType.ChannelMessageWithSource, builder) |> Async.AwaitTask + } + type Store() = inherit ApplicationCommandModule () @@ -294,8 +287,7 @@ type Store() = member this.SellShield (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (sell "Shields" (Inventory.getItemsByType ItemType.Shield)) [] - member this.Consume (ctx : InteractionContext) = - enforceChannel (DiscordInteractionContext ctx) (sell "Food" (Inventory.getItemsByType ItemType.Food)) + member this.Consume (ctx : InteractionContext) = consume (DiscordInteractionContext ctx) [] member this.Inventory (ctx : InteractionContext) = diff --git a/Bot/Games/Trainer.fs b/Bot/Games/Trainer.fs index 4549a86..2473c91 100644 --- a/Bot/Games/Trainer.fs +++ b/Bot/Games/Trainer.fs @@ -9,8 +9,10 @@ open Degenz.Messaging let TrainerAchievement = "FINISHED_TRAINER" let Sensei = { Id = GuildEnvironment.botIdHackerBattle ; Name = "Sensei" } -let defaultHack = Armory.weapons |> Inventory.findHackById (int ItemId.Virus) -let defaultShield = Armory.weapons |> Inventory.findShieldById (int ItemId.Firewall) +let hackItem = Armory.weapons |> Inventory.findItemById (int ItemId.Virus) +let shieldItem = Armory.weapons |> Inventory.findItemById (int ItemId.Firewall) +let defaultHack = (Inventory.getHackItem hackItem).Value +let defaultShield = (Inventory.getShieldItem shieldItem ).Value let CurrencyGift = 250 let BeginnerProtectionHours = 24 @@ -21,7 +23,7 @@ let HackEvent () = { Adversary = Sensei Success = true IsInstigator = true - HackId = defaultHack.Item.Id + HackId = defaultHack.Id } } let ShieldEvents () = [ @@ -63,7 +65,7 @@ let handleTrainerStep1 (ctx : IDiscordContext) = |> Async.AwaitTask let msg = "BeautopiaΒ© is a dangerous place... quick, put up a SHIELD πŸ›‘ before another Degen hacks you, and takes your πŸ’°$GBT.\n\n" + "To enable it, you need to run the `/shield` slash command.\n\n" - + $"Type the `/shield` command now, then select - `{defaultShield.Item.Name}`\n" + + $"Type the `/shield` command now, then select - `{defaultShield.Name}`\n" let builder = DiscordInteractionResponseBuilder() .WithContent(msg) @@ -78,7 +80,7 @@ let defend (ctx : IDiscordContext) = do! Messaging.defer ctx let m = ctx.GetDiscordMember() let name = if System.String.IsNullOrEmpty m.Nickname then m.DisplayName else m.Nickname - let embed = Embeds.pickDefense "Trainer-2" { PlayerData.empty with Inventory = [ Shield defaultShield ] ; Name = name } true + let embed = Embeds.pickDefense "Trainer-2" { PlayerData.empty with Inventory = [ shieldItem ] ; Name = name } true do! ctx.FollowUp(embed) |> Async.AwaitTask do! Analytics.trainingDojoStep "DefendCommand" (ctx.GetDiscordMember().Id) (ctx.GetDiscordMember().Username) } |> Async.StartAsTask :> Task @@ -101,11 +103,11 @@ let handleDefense (ctx : IDiscordContext) = let embed = Embeds.responseCreatedShield defaultShield do! ctx.FollowUp embed |> Async.AwaitTask do! Async.Sleep 1000 - do! sendMessage' $"Ok, good, let me make sure that worked.\n\nI'll try to **hack** you now with **{defaultHack.Item.Name}**" + do! sendMessage' $"Ok, good, let me make sure that worked.\n\nI'll try to **hack** you now with **{defaultHack.Name}**" do! Async.Sleep 3000 do! sendMessage' $"❌ HACKING FAILED!\n\n{playerName} defended hack from <@{Sensei.Id}>!" do! Async.Sleep 1500 - do! sendFollowUpMessageWithButton ctx (handleDefenseMsg defaultHack.Item.Name) + do! sendFollowUpMessageWithButton ctx (handleDefenseMsg defaultHack.Name) do! Analytics.trainingDojoStep "ShieldActivated" (ctx.GetDiscordMember().Id) (ctx.GetDiscordMember().Username) } |> Async.StartAsTask :> Task @@ -117,7 +119,7 @@ let handleTrainerStep3 (ctx : IDiscordContext) = .WithContent ( "Now let’s **HACK** πŸ’»... I want you to **HACK ME**!\n\n" + "To **hack**, you need to run the `/hack` slash command.\n" - + $"Type the `/hack` command now, then choose me - <@{Sensei.Id}> as your target, and select `{defaultHack.Item.Name}`") + + $"Type the `/hack` command now, then choose me - <@{Sensei.Id}> as your target, and select `{defaultHack.Name}`") do! ctx.Respond(InteractionResponseType.ChannelMessageWithSource, builder) |> Async.AwaitTask do! Analytics.trainingDojoStep "LetsHack" (ctx.GetDiscordMember().Id) (ctx.GetDiscordMember().Username) @@ -130,7 +132,7 @@ let hack (target : DiscordUser) (ctx : IDiscordContext) = let isRightTarget = target.Id = Sensei.Id match isRightTarget with | true -> - let player = { PlayerData.empty with Inventory = [ Hack defaultHack ] } + let player = { PlayerData.empty with Inventory = [ hackItem ] } let bot = { PlayerData.empty with DiscordId = Sensei.Id ; Name = Sensei.Name } let embed = Embeds.pickHack "Trainer-4" player bot true @@ -166,8 +168,8 @@ let handleHack (ctx : IDiscordContext) = **🎁 FREE GIFTS:** Here, I'm going to gift you: -1. A hack - `{defaultHack.Item.Name}`, -2. A shield - `{defaultShield.Item.Name}`, +1. A hack - `{defaultHack.Name}`, +2. A shield - `{defaultShield.Name}`, 3. `{CurrencyGift} πŸ’° $GBT`, I'm also going to give you some **TEMPORARY SHIELDS** until you buy your own. @@ -194,7 +196,7 @@ let handleArsenal (ctx : IDiscordContext) = PlayerInteractions.executePlayerActi let! completed = DbService.checkHasAchievement player.DiscordId TrainerAchievement if not completed then do! DbService.addAchievement player.DiscordId TrainerAchievement |> Async.Ignore - let addIfDoesntExist (weapon : ItemDetails) (inventory : Inventory) = + let addIfDoesntExist (weapon : Item) (inventory : Inventory) = if inventory |> List.exists (fun i -> i.Id = weapon.Id) then inventory else weapon::inventory @@ -209,8 +211,8 @@ let handleArsenal (ctx : IDiscordContext) = PlayerInteractions.executePlayerActi |> List.consTo (HackEvent()) Inventory = player.Inventory - |> addIfDoesntExist (Hack defaultHack) - |> addIfDoesntExist (Shield defaultShield) + |> addIfDoesntExist hackItem + |> addIfDoesntExist shieldItem Bank = player.Bank + CurrencyGift } do! DbService.updatePlayer updatedPlayer |> Async.Ignore @@ -227,7 +229,7 @@ let handleArsenal (ctx : IDiscordContext) = PlayerInteractions.executePlayerActi do! ctx.FollowUp(embed) |> Async.AwaitTask do! Async.Sleep 2000 - let rewards = [ $"{defaultHack.Item.Name} Hack" ; $"{defaultShield.Item.Name} Shield" ; $"{CurrencyGift} πŸ’°$GBT"] + let rewards = [ $"{defaultHack.Name} Hack" ; $"{defaultShield.Name} Shield" ; $"{CurrencyGift} πŸ’°$GBT"] let embed = Embeds.getAchievementEmbed rewards "You completed the Training Dojo and collected some gifts." TrainerAchievement do! ctx.FollowUp(embed) |> Async.AwaitTask diff --git a/Bot/GuildEnvironment.fs b/Bot/GuildEnvironment.fs index 73a20e0..58b2952 100644 --- a/Bot/GuildEnvironment.fs +++ b/Bot/GuildEnvironment.fs @@ -52,3 +52,4 @@ let roleAdmin = getId "ROLE_ADMIN" let mutable botClientRecruit : DiscordClient option = None let mutable botClientHacker : DiscordClient option = None let mutable botClientSlots : DiscordClient option = None +let mutable botClientStore : DiscordClient option = None From 6dda9dd8a36a1326c1dfd8585bca1489fa43493b Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Thu, 28 Apr 2022 18:28:28 +0700 Subject: [PATCH 06/10] Update whitelist and jackpot to use new tables --- Bot/DbService.fs | 4 ++-- Bot/GameHelpers.fs | 1 + Bot/GameTypes.fs | 1 + Bot/Games/SlotMachine.fs | 15 ++++++++++++--- Bot/InviteTracker.fs | 12 ++++++++---- 5 files changed, 24 insertions(+), 9 deletions(-) diff --git a/Bot/DbService.fs b/Bot/DbService.fs index 7ae2e5c..7c9a58d 100644 --- a/Bot/DbService.fs +++ b/Bot/DbService.fs @@ -29,7 +29,7 @@ let readItem (reader : RowReader) = | "Shield" -> ItemType.Shield | "Food" -> ItemType.Food | "Accessory" -> ItemType.Accessory - | _ -> ItemType.Accessory + | _ -> ItemType.Misc Item.Attributes = [ reader.intOrNone "buy_price" |> Option.map (fun a -> Buyable (a * 1)) reader.intOrNone "sell_price" |> Option.map (fun a -> Sellable (a * 1)) @@ -185,7 +185,7 @@ let updatePlayerCurrency (addAmount : int) (player : PlayerData) = "did", Sql.string (string player.DiscordId) "gbt", Sql.int (int addAmount) ] |> Sql.query """ - UPDATE "user" SET gbt = gbt + @gbt WHERE discord_id = @did; + UPDATE "user" SET gbt = gbt + GREATEST(gbt + @gbt, 0) WHERE discord_id = @did; """ |> Sql.executeNonQueryAsync |> Async.AwaitTask diff --git a/Bot/GameHelpers.fs b/Bot/GameHelpers.fs index 5a0dc5f..8fb61f4 100644 --- a/Bot/GameHelpers.fs +++ b/Bot/GameHelpers.fs @@ -19,6 +19,7 @@ module Inventory = | ItemType.Shield -> inventory |> List.filter (fun item -> match item.Type with ItemType.Shield _ -> true | _ -> false) | ItemType.Food -> inventory |> List.filter (fun item -> match item.Type with ItemType.Food _ -> true | _ -> false) | ItemType.Accessory -> inventory |> List.filter (fun item -> match item.Type with ItemType.Accessory _ -> true | _ -> false) + | ItemType.Misc -> inventory |> List.filter (fun item -> match item.Type with ItemType.Misc _ -> true | _ -> false) let findItemById id (inventory : Inventory) = inventory |> List.find (fun item -> item.Id = id) diff --git a/Bot/GameTypes.fs b/Bot/GameTypes.fs index ddf003f..472a297 100644 --- a/Bot/GameTypes.fs +++ b/Bot/GameTypes.fs @@ -88,6 +88,7 @@ type ItemType = | Shield | Food | Accessory + | Misc type Effect = | Min of int diff --git a/Bot/Games/SlotMachine.fs b/Bot/Games/SlotMachine.fs index c7ed238..80317dd 100644 --- a/Bot/Games/SlotMachine.fs +++ b/Bot/Games/SlotMachine.fs @@ -128,7 +128,10 @@ let mutable anyEmoji : DiscordEmoji option = None let getJackpotAmount () = GuildEnvironment.connectionString |> Sql.connect - |> Sql.query "SELECT stock FROM item WHERE symbol = 'JACKPOT'" + |> Sql.query """ + SELECT stock FROM store_item + WHERE store_item.item_id = (SELECT id FROM item WHERE symbol = 'JACKPOT') + """ |> Sql.executeRowAsync (fun read -> (read.int "stock") * 1) |> Async.AwaitTask @@ -136,7 +139,10 @@ let incrementJackpot amount = GuildEnvironment.connectionString |> Sql.connect |> Sql.parameters [ ( "amount" , Sql.int (int amount) ) ] - |> Sql.query "UPDATE item SET stock = stock + @amount WHERE symbol = 'JACKPOT'" + |> Sql.query """ + UPDATE store_item SET stock = stock + @amount + WHERE store_item.item_id = (SELECT id FROM item WHERE symbol = 'JACKPOT') + """ |> Sql.executeNonQueryAsync |> Async.AwaitTask @@ -144,7 +150,10 @@ let resetJackpot amount = GuildEnvironment.connectionString |> Sql.connect |> Sql.parameters [ ( "amount" , Sql.int (int amount) ) ] - |> Sql.query "UPDATE item SET stock = @amount WHERE symbol = 'JACKPOT'" + |> Sql.query """ + UPDATE store_item SET stock = @amount + WHERE store_item.item_id = (SELECT id FROM item WHERE symbol = 'JACKPOT') + """ |> Sql.executeNonQueryAsync |> Async.AwaitTask diff --git a/Bot/InviteTracker.fs b/Bot/InviteTracker.fs index e170fba..68f7a2f 100644 --- a/Bot/InviteTracker.fs +++ b/Bot/InviteTracker.fs @@ -182,9 +182,11 @@ let getWhitelistItem () = connStr |> Sql.connect |> Sql.query """ - SELECT stock, price FROM item WHERE symbol = 'WHITELIST' + SELECT stock, buy_price FROM store_item + JOIN item i on store_item.item_id = i.id + WHERE i.symbol = 'WHITELIST' """ - |> Sql.executeRowAsync (fun read -> {| Stock = read.int "stock" ; Price = (read.int "price") * 1 |}) + |> Sql.executeRowAsync (fun read -> {| Stock = read.int "stock" ; Price = (read.int "buy_price") * 1 |}) |> Async.AwaitTask let updateWhitelistStock () = async { @@ -192,7 +194,8 @@ let updateWhitelistStock () = async { do! connStr |> Sql.connect |> Sql.query """ - UPDATE item SET stock = stock - 1 WHERE symbol = 'WHITELIST' + UPDATE store_item SET stock = GREATEST(stock - 1, 0) + WHERE store_item.item_id = (SELECT id FROM item WHERE symbol = 'WHITELIST') """ |> Sql.executeNonQueryAsync |> Async.AwaitTask @@ -207,7 +210,8 @@ let setWhitelistStock amount = async { |> Sql.connect |> Sql.parameters [ ( "amount" , Sql.int amount ) ] |> Sql.query """ - UPDATE item SET stock = @amount WHERE symbol = 'WHITELIST' + UPDATE store_item SET stock = @amount + WHERE store_item.item_id = (SELECT id FROM item WHERE symbol = 'WHITELIST') """ |> Sql.executeNonQueryAsync |> Async.AwaitTask From f21d4df854eac8545ac8cc1543d4330e6e2bcabd Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 29 Apr 2022 11:06:24 +0700 Subject: [PATCH 07/10] Set things up for the NFT store --- Bot/Admin.fs | 9 ++ Bot/Analytics.fs | 12 +- Bot/Bot.fs | 7 +- Bot/Bot.fsproj | 3 - Bot/DbService.fs | 41 ++++- Bot/GameHelpers.fs | 17 +- Bot/GameTypes.fs | 3 + Bot/Games/HackerBattle.fs | 7 +- Bot/Games/Store.fs | 120 ++++++++++---- Bot/Games/Trainer.fs | 5 +- Bot/Items.json | 318 -------------------------------------- paket.dependencies | 6 +- paket.lock | 10 +- 13 files changed, 169 insertions(+), 389 deletions(-) delete mode 100644 Bot/Items.json diff --git a/Bot/Admin.fs b/Bot/Admin.fs index ad2fa71..33afd11 100644 --- a/Bot/Admin.fs +++ b/Bot/Admin.fs @@ -2,6 +2,7 @@ module Degenz.Admin open System open System.IO +open System.Reflection open System.Threading.Tasks open DSharpPlus open DSharpPlus.Entities @@ -14,6 +15,7 @@ type InitEmbeds = | Dojo = 0 | Whitelist = 1 | Slots = 2 + | JpegStore = 3 let handleGuildDownloadReady (_ : DiscordClient) (event : GuildDownloadCompletedEventArgs) = task { @@ -24,6 +26,7 @@ let handleGuildDownloadReady (_ : DiscordClient) (event : GuildDownloadCompleted let permission = DiscordApplicationCommandPermission(adminRole, true) let commands = commands |> Seq.map (fun com -> DiscordGuildApplicationCommandPermissions(com.Id, [ permission ])) do! guild.BatchEditApplicationCommandPermissionsAsync(commands) |> Async.AwaitTask |> Async.Ignore + return () } :> Task let sendEmbed embed (ctx : IDiscordContext) = @@ -32,6 +35,7 @@ let sendEmbed embed (ctx : IDiscordContext) = | InitEmbeds.Dojo -> Trainer.sendInitialEmbed ctx | InitEmbeds.Whitelist -> InviteTracker.sendInitialEmbed ctx | InitEmbeds.Slots -> SlotMachine.sendInitialEmbedFromSlashCommand ctx + | InitEmbeds.JpegStore -> Store.sendInitialEmbed ctx | _ -> () do! Messaging.sendSimpleResponse ctx "Sent!" } :> Task @@ -137,24 +141,29 @@ type AdminBot() = else Messaging.sendSimpleResponse ctx $"You are not admin" |> Async.StartAsTask :> Task + [] [] member this.GetAttributions (ctx : InteractionContext, [] user : DiscordUser) = enforceAdmin (DiscordInteractionContext ctx) (InviteTracker.getInvitedUsersForId user) + [] [] member this.SetStock (ctx : InteractionContext, [] amount : int64) = enforceAdmin (DiscordInteractionContext ctx) (InviteTracker.setCurrentWhitelistStock (int amount)) + [] [] member this.SendEmbedToChannel (ctx : InteractionContext, [] embed : InitEmbeds) = enforceAdmin (DiscordInteractionContext ctx) (sendEmbed embed) + [] [] member this.GetMessageReactions (ctx : InteractionContext, [] channel : DiscordChannel, [] messageId : string) = enforceAdmin (DiscordInteractionContext ctx) (getUsersFromMessageReactions channel messageId) + [] [] member this.GetInvitesFromReactedMessages (ctx : InteractionContext, [] channel : DiscordChannel, diff --git a/Bot/Analytics.fs b/Bot/Analytics.fs index 58dd3a4..1a19b85 100644 --- a/Bot/Analytics.fs +++ b/Bot/Analytics.fs @@ -94,19 +94,19 @@ let arsenalCommand (discordMember : DiscordMember) = ] track "Arsenal Command Invoked" discordMember.Id data -let buyWeaponCommand (discordMember : DiscordMember) weaponType = +let buyItemCommand (discordMember : DiscordMember) store = let data = [ "user_display_name" , discordMember.Username - "weapon_type" , string weaponType + "store_symbol" , store ] - track "Buy Weapon Command Invoked" discordMember.Id data + track "Buy Item Command Invoked" discordMember.Id data -let sellWeaponCommand (discordMember : DiscordMember) weaponType = +let sellItemCommand (discordMember : DiscordMember) store = let data = [ "user_display_name" , discordMember.Username - "weapon_type" , string weaponType + "store_symbol" , store ] - track "Sell Weapon Command Invoked" discordMember.Id data + track "Sell Item Command Invoked" discordMember.Id data let buyWeaponButton (discordMember : DiscordMember) itemName itemPrice = let data = [ diff --git a/Bot/Bot.fs b/Bot/Bot.fs index 71bafd2..1b5743f 100644 --- a/Bot/Bot.fs +++ b/Bot/Bot.fs @@ -77,7 +77,6 @@ inviterBot.add_ComponentInteractionCreated(AsyncEventHandler(InviteTracker.handl slotsBot.add_ComponentInteractionCreated(AsyncEventHandler(SlotMachine.handleButton)) slotsBot.add_GuildDownloadCompleted(AsyncEventHandler(SlotMachine.handleGuildDownloadCompleted)) slotsBot.add_MessageCreated(AsyncEventHandler(SlotMachine.handleMessageCreated)) -adminBot.add_GuildDownloadCompleted(AsyncEventHandler(Admin.handleGuildDownloadReady)) let asdf (_ : DiscordClient) (event : DSharpPlus.EventArgs.InteractionCreateEventArgs) = async { @@ -95,15 +94,15 @@ let asdf (_ : DiscordClient) (event : DSharpPlus.EventArgs.InteractionCreateEven :> Task //hackerBattleBot.add_InteractionCreated(AsyncEventHandler(asdf)) +storeBot.ConnectAsync() |> Async.AwaitTask |> Async.RunSynchronously +GuildEnvironment.botClientStore <- Some storeBot + slotsBot.ConnectAsync() |> Async.AwaitTask |> Async.RunSynchronously GuildEnvironment.botClientSlots <- Some slotsBot hackerBattleBot.ConnectAsync() |> Async.AwaitTask |> Async.RunSynchronously GuildEnvironment.botClientHacker <- Some hackerBattleBot -storeBot.ConnectAsync() |> Async.AwaitTask |> Async.RunSynchronously -//GuildEnvironment.botClient <- Some storeBot.CurrentUser - inviterBot.ConnectAsync() |> Async.AwaitTask |> Async.RunSynchronously GuildEnvironment.botClientRecruit <- Some inviterBot diff --git a/Bot/Bot.fsproj b/Bot/Bot.fsproj index 0edeab0..f13dcd5 100644 --- a/Bot/Bot.fsproj +++ b/Bot/Bot.fsproj @@ -7,9 +7,6 @@ Degenz - - PreserveNewest - diff --git a/Bot/DbService.fs b/Bot/DbService.fs index 7c9a58d..6295aad 100644 --- a/Bot/DbService.fs +++ b/Bot/DbService.fs @@ -23,12 +23,15 @@ let readItem (reader : RowReader) = { TargetStat = stat ; Effect = fx } { Item.Id = reader.int "id" Item.Name = reader.string "name" + Item.IconUrl = reader.string "icon" + Item.Symbol = reader.string "symbol" Item.Type = match reader.string "category" with | "Hack" -> ItemType.Hack | "Shield" -> ItemType.Shield | "Food" -> ItemType.Food | "Accessory" -> ItemType.Accessory + | "Jpeg" -> ItemType.Jpeg | _ -> ItemType.Misc Item.Attributes = [ reader.intOrNone "buy_price" |> Option.map (fun a -> Buyable (a * 1)) @@ -53,7 +56,7 @@ let getPlayerInventory (did : uint64) = |> Sql.connect |> Sql.parameters [ "did", Sql.string (string did) ] |> Sql.query """ - SELECT ii.id,name,category,buy_price,sell_price,rate_limit,expiration,drop_chance,can_trade,can_consume, + SELECT ii.id,ii.symbol,name,icon,category,buy_price,sell_price,rate_limit,expiration,drop_chance,can_trade,can_consume, attack_power,defense_power,class_name,max_stack,mods FROM inventory_item JOIN item ii on inventory_item.item_id = ii.id @@ -74,12 +77,22 @@ let addToPlayerInventory (did : uint64) (item : Item) = |> Sql.executeNonQueryAsync |> Async.AwaitTask +let getStoreSymbol (channelId : uint64) = + connStr + |> Sql.connect + |> Sql.parameters [ "cid", Sql.string (string channelId) ] + |> Sql.query """ + SELECT symbol FROM store WHERE channel_id = @cid + """ + |> Sql.executeRowAsync (fun read -> read.string "symbol") + |> Async.AwaitTask + let getStoreItems (channelId : uint64) = connStr |> Sql.connect |> Sql.parameters [ "cid", Sql.string (string channelId) ] |> Sql.query """ - SELECT i.id,name,category,buy_price,sell_price,rate_limit,expiration,drop_chance,can_trade,can_consume, + SELECT i.id,i.symbol,name,icon,category,buy_price,sell_price,rate_limit,expiration,drop_chance,can_trade,can_consume, attack_power,defense_power,class_name,max_stack,mods FROM store_item JOIN store st on store_item.store_id = st.id @@ -89,6 +102,30 @@ let getStoreItems (channelId : uint64) = |> Sql.executeAsync readItem |> Async.AwaitTask +let getWeapons () = + connStr + |> Sql.connect + |> Sql.query """ + SELECT i.id,i.symbol,name,icon,category,buy_price,sell_price,rate_limit,expiration,drop_chance,can_trade,can_consume, + attack_power,defense_power,class_name,max_stack,mods + FROM item WHERE category = 'Hack' OR symbol = 'Shield' + """ + |> Sql.executeAsync readItem + |> Async.AwaitTask + +let consumeItem (did : uint64) (item : Item) = + connStr + |> Sql.connect + |> Sql.parameters [ ( "@did" , Sql.string (string did) ) ; ( "iid" , Sql.int item.Id )] + |> Sql.query """ + DELETE FROM inventory_item + WHERE id IN (SELECT id FROM inventory_item + WHERE user_id = (SELECT id FROM "user" WHERE discord_id = @did) + AND item_id = @iid LIMIT 1) + """ + |> Sql.executeNonQueryAsync + |> Async.AwaitTask + let getPlayerEvents (did : uint64) = connStr |> Sql.connect diff --git a/Bot/GameHelpers.fs b/Bot/GameHelpers.fs index 8fb61f4..fc1f0d8 100644 --- a/Bot/GameHelpers.fs +++ b/Bot/GameHelpers.fs @@ -6,12 +6,6 @@ open DSharpPlus.Entities open Degenz open Newtonsoft.Json -module Armory = - let weapons : Inventory = - let file = System.IO.File.ReadAllText("Items.json") -// let file = System.IO.File.ReadAllText("Bot/Items.json") - JsonConvert.DeserializeObject(file) - module Inventory = let getItemsByType itemType inventory = match itemType with @@ -19,6 +13,7 @@ module Inventory = | ItemType.Shield -> inventory |> List.filter (fun item -> match item.Type with ItemType.Shield _ -> true | _ -> false) | ItemType.Food -> inventory |> List.filter (fun item -> match item.Type with ItemType.Food _ -> true | _ -> false) | ItemType.Accessory -> inventory |> List.filter (fun item -> match item.Type with ItemType.Accessory _ -> true | _ -> false) + | ItemType.Jpeg -> inventory |> List.filter (fun item -> match item.Type with ItemType.Jpeg _ -> true | _ -> false) | ItemType.Misc -> inventory |> List.filter (fun item -> match item.Type with ItemType.Misc _ -> true | _ -> false) let findItemById id (inventory : Inventory) = inventory |> List.find (fun item -> item.Id = id) @@ -143,7 +138,7 @@ module Arsenal = | [] -> "None" | _ -> items |> List.map (fun item -> item.Name) |> String.concat ", " - let actionFormat (actions : PlayerEvent List) = + let actionFormat items (actions : PlayerEvent List) = match actions with | [] -> "None" | acts -> @@ -151,11 +146,11 @@ module Arsenal = |> List.map (fun event -> match event.Type with | Hacking h -> - let item = Armory.weapons |> Inventory.findItemById h.HackId + let item = items |> Inventory.findItemById h.HackId let cooldown = Messaging.getTimeText false WeaponClass.SameTargetAttackCooldown event.Timestamp $"Hacked {h.Adversary.Name} with {item.Name} {cooldown} ago" | Shielding id -> - let item = Armory.weapons |> Inventory.findItemById id + let item = items |> Inventory.findItemById id let cooldown = Messaging.getTimeText true (TimeSpan.FromMinutes(int event.Cooldown)) event.Timestamp $"{item.Name} Shield active for {cooldown}" | _ -> "") @@ -166,5 +161,5 @@ module Arsenal = let hacks = Player.getHackEvents p $"**Hacks:** {Inventory.getItemsByType ItemType.Hack p.Inventory |> battleItemFormat}\n **Shields:** {Inventory.getItemsByType ItemType.Shield p.Inventory |> battleItemFormat}\n - **Hack Attacks:**\n{hacks |> List.take (min hacks.Length 10) |> actionFormat}\n - **Active Shields:**\n{Player.getShieldEvents p |> actionFormat}" + **Hack Attacks:**\n{hacks |> List.take (min hacks.Length 10) |> actionFormat p.Inventory}\n + **Active Shields:**\n{Player.getShieldEvents p |> actionFormat p.Inventory}" diff --git a/Bot/GameTypes.fs b/Bot/GameTypes.fs index 472a297..6feb097 100644 --- a/Bot/GameTypes.fs +++ b/Bot/GameTypes.fs @@ -88,6 +88,7 @@ type ItemType = | Shield | Food | Accessory + | Jpeg | Misc type Effect = @@ -132,6 +133,8 @@ type Item = { Id : int Name : string Type : ItemType + Symbol : string + IconUrl : string Attributes : ItemAttribute list } diff --git a/Bot/Games/HackerBattle.fs b/Bot/Games/HackerBattle.fs index 9a3b8b3..9374bbc 100644 --- a/Bot/Games/HackerBattle.fs +++ b/Bot/Games/HackerBattle.fs @@ -83,7 +83,7 @@ let runHackerBattle defender (hack : HackItem) = |> List.exists (fun event -> match event.Type with | Shielding id -> - let item = Inventory.findItemById id Armory.weapons + let item = Inventory.findItemById id Trainer.weapons match item.Attributes with | CanClass c -> hack.Class = c | _ -> false @@ -165,7 +165,8 @@ let handleAttack (ctx : IDiscordContext) = executePlayerAction ctx (fun attacker -> async { let tokens = ctx.GetInteractionId().Split("-") let hackId = int tokens.[1] - let item = Armory.weapons |> Inventory.findItemById hackId + // TODO: This sucks + let item = Trainer.weapons |> Inventory.findItemById hackId let hackItem = (Inventory.getHackItem item).Value let resultId , targetId = UInt64.TryParse tokens.[2] let! resultTarget = DbService.tryFindPlayer targetId @@ -206,7 +207,7 @@ let handleDefense (ctx : IDiscordContext) = executePlayerAction ctx (fun player -> async { let tokens = ctx.GetInteractionId().Split("-") let shieldId = int tokens.[1] - let item = Armory.weapons |> Inventory.findItemById shieldId + let item = Trainer.weapons |> Inventory.findItemById shieldId let shieldItem = (Inventory.getShieldItem item).Value do! player diff --git a/Bot/Games/Store.fs b/Bot/Games/Store.fs index 452a187..9e2d621 100644 --- a/Bot/Games/Store.fs +++ b/Bot/Games/Store.fs @@ -52,7 +52,7 @@ let getItemEmbeds owned (items : Inventory) = |> ignore match Embeds.getItemIcon item.Id with | Some url -> embed.WithThumbnail(url) - | None -> embed) + | None -> if String.IsNullOrWhiteSpace(item.IconUrl) then embed else embed.WithThumbnail(item.IconUrl)) |> List.map (fun e -> e.Build()) |> Seq.ofList @@ -66,10 +66,15 @@ let getBuyItemsEmbed (playerInventory : Inventory) (storeInventory : Inventory) else DiscordButtonComponent(WeaponClass.getClassButtonColor item, $"Buy-{item.Id}", $"Buy {item.Name}") :> DiscordComponent) - DiscordFollowupMessageBuilder() - .AddEmbeds(embeds) - .AddComponents(buttons) - .AsEphemeral(true) + let builder = + DiscordFollowupMessageBuilder() + .AddEmbeds(embeds) + .AsEphemeral(true) + buttons + |> List.chunkBySize 5 + |> List.iter (fun btns -> builder.AddComponents(btns) |> ignore) + builder + let getSellEmbed (items : Inventory) = let embeds , buttons = @@ -126,15 +131,16 @@ let checkHasItemsInArsenal itemType items player = then Ok player else Error $"You currently have no {itemType} in your arsenal to sell!" -let buy itemType (ctx : IDiscordContext) = +let buy (ctx : IDiscordContext) = executePlayerAction ctx (fun player -> async { - let playerItems = Inventory.getItemsByType itemType player.Inventory try - let! armoryItems = DbService.getStoreItems (ctx.GetChannel().Id) - if armoryItems.Length > 0 then - let itemStore = getBuyItemsEmbed playerItems armoryItems + let channelId = ctx.GetChannel().Id + let! items = DbService.getStoreItems channelId + if items.Length > 0 then + let itemStore = getBuyItemsEmbed player.Inventory items do! ctx.FollowUp itemStore |> Async.AwaitTask - do! Analytics.buyWeaponCommand (ctx.GetDiscordMember()) itemType + let! storeSymbol = DbService.getStoreSymbol channelId + do! Analytics.buyItemCommand (ctx.GetDiscordMember()) storeSymbol else do! Messaging.sendFollowUpMessage ctx "This channel doesn't have anything to sell" with ex -> printfn $"{ex.Message}" @@ -147,9 +153,22 @@ let sell itemType getItems (ctx : IDiscordContext) = | Ok _ -> let itemStore = getSellEmbed items do! ctx.FollowUp(itemStore) |> Async.AwaitTask | Error e -> do! sendFollowUpMessage ctx e - do! Analytics.sellWeaponCommand (ctx.GetDiscordMember()) itemType + do! Analytics.buyItemCommand (ctx.GetDiscordMember()) itemType }) +let purchaseItemEmbed (item : Item) = + let embed = DiscordEmbedBuilder() + embed.ImageUrl <- item.IconUrl + embed.Title <- $"Purchased {item.Name}" + match item.Type with + | ItemType.Jpeg -> + if item.Symbol = "RAFFLE" then + embed.Description <- $"Congratulations! You are in the draw for the {item.Name}. The winner will be announced shortly" + 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 + // TODO: When you buy a shield, prompt the user to activate it let handleBuyItem (ctx : IDiscordContext) itemId = executePlayerAction ctx (fun player -> async { @@ -162,14 +181,17 @@ let handleBuyItem (ctx : IDiscordContext) itemId = let price = match item.Attributes with CanBuy price -> price | _ -> 0 do! DbService.updatePlayerCurrency -price player |> Async.Ignore do! DbService.addToPlayerInventory player.DiscordId item |> Async.Ignore - do! sendFollowUpMessage ctx $"Successfully purchased {item.Name}! You now have {player.Bank - price} πŸ’°$GBT remaining" +// do! ctx.FollowUp $"Successfully purchased {item.Name}! You now have {player.Bank - price} πŸ’°$GBT remaining" + 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 = Armory.weapons |> Inventory.findItemById itemId + let item = player.Inventory |> Inventory.findItemById itemId do! player |> checkSoldItemAlready item @@ -201,8 +223,19 @@ let consume (ctx : IDiscordContext) = PlayerInteractions.executePlayerAction ctx do! ctx.FollowUp builder |> Async.AwaitTask }) -let showInventoryEmbed (ctx : IDiscordContext) = PlayerInteractions.executePlayerAction ctx (fun player -> async { - let embeds = getItemEmbeds true player.Inventory +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 embeds = getItemEmbeds true items + let builder = DiscordFollowupMessageBuilder().AddEmbeds(embeds).AsEphemeral(true) + do! ctx.FollowUp builder |> Async.AwaitTask +}) + +let showJpegsEmbed (ctx : IDiscordContext) = PlayerInteractions.executePlayerAction ctx (fun player -> async { + let jpegs = player.Inventory |> Inventory.getItemsByType ItemType.Jpeg + let embeds = getItemEmbeds true jpegs let builder = DiscordFollowupMessageBuilder().AddEmbeds(embeds).AsEphemeral(true) do! ctx.FollowUp builder |> Async.AwaitTask }) @@ -236,6 +269,8 @@ let handleStoreEvents (_ : DiscordClient) (event : ComponentInteractionCreateEve 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 ctx | _ -> task { let builder = DiscordInteractionResponseBuilder() @@ -244,6 +279,27 @@ let handleStoreEvents (_ : DiscordClient) (event : ComponentInteractionCreateEve do! ctx.Respond(InteractionResponseType.ChannelMessageWithSource, builder) |> Async.AwaitTask } +let sendInitialEmbed (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 <- "Degenz Game" + 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", $"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 + type Store() = inherit ApplicationCommandModule () @@ -257,28 +313,28 @@ type Store() = } // 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 - } +// 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 +// } [] - member _.BuyItem (ctx : InteractionContext) = checkChannel (DiscordInteractionContext ctx) + member _.BuyItem (ctx : InteractionContext) = buy (DiscordInteractionContext ctx) [] member _.BuyHack (ctx : InteractionContext) = - enforceChannel (DiscordInteractionContext(ctx)) (buy ItemType.Hack) + enforceChannel (DiscordInteractionContext(ctx)) buy [] member this.BuyShield (ctx : InteractionContext) = - enforceChannel (DiscordInteractionContext(ctx)) (buy ItemType.Shield) + enforceChannel (DiscordInteractionContext(ctx)) buy [] member this.SellHack (ctx : InteractionContext) = enforceChannel (DiscordInteractionContext(ctx)) (sell "Hacks" (Inventory.getItemsByType ItemType.Hack)) @@ -289,9 +345,9 @@ type Store() = [] member this.Consume (ctx : InteractionContext) = consume (DiscordInteractionContext ctx) - [] + [] member this.Inventory (ctx : InteractionContext) = - showInventoryEmbed (DiscordInteractionContext ctx) + showJpegsEmbed (DiscordInteractionContext ctx) [] member this.Stats (ctx : InteractionContext) = diff --git a/Bot/Games/Trainer.fs b/Bot/Games/Trainer.fs index 2473c91..7a32086 100644 --- a/Bot/Games/Trainer.fs +++ b/Bot/Games/Trainer.fs @@ -9,8 +9,9 @@ open Degenz.Messaging let TrainerAchievement = "FINISHED_TRAINER" let Sensei = { Id = GuildEnvironment.botIdHackerBattle ; Name = "Sensei" } -let hackItem = Armory.weapons |> Inventory.findItemById (int ItemId.Virus) -let shieldItem = Armory.weapons |> Inventory.findItemById (int ItemId.Firewall) +let weapons = DbService.getWeapons () |> Async.RunSynchronously +let hackItem = weapons |> Inventory.findItemById (int ItemId.Virus) +let shieldItem = weapons |> Inventory.findItemById (int ItemId.Firewall) let defaultHack = (Inventory.getHackItem hackItem).Value let defaultShield = (Inventory.getShieldItem shieldItem ).Value let CurrencyGift = 250 diff --git a/Bot/Items.json b/Bot/Items.json deleted file mode 100644 index 0012b85..0000000 --- a/Bot/Items.json +++ /dev/null @@ -1,318 +0,0 @@ -[ - { - "Case": "Hack", - "Fields": [ - { - "Power": 20, - "Class": 0, - "Cooldown": 2, - "Item": { - "Id": 0, - "Name": "Virus", - "Price": 250, - "MaxAllowed": 1, - "Attributes": { - "CanBuy" : true, - "CanSell" : true, - "CanConsume" : false, - "CanTrade" : false, - "CanDrop" : false - } - } - } - ] - }, - { - "Case": "Hack", - "Fields": [ - { - "Power": 20, - "Class": 1, - "Cooldown": 2, - "Item": { - "Id": 1, - "Name": "Remote Access", - "Price": 250, - "MaxAllowed": 1, - "Attributes": { - "CanBuy" : true, - "CanSell" : true, - "CanConsume" : false, - "CanTrade" : false, - "CanDrop" : false - } - } - } - ] - }, - { - "Case": "Hack", - "Fields": [ - { - "Power": 20, - "Class": 2, - "Cooldown": 2, - "Item": { - "Id": 2, - "Name": "Worm", - "Price": 250, - "MaxAllowed": 1, - "Attributes": { - "CanBuy" : true, - "CanSell" : true, - "CanConsume" : false, - "CanTrade" : false, - "CanDrop" : false - } - } - } - ] - }, - { - "Case": "Shield", - "Fields": [ - { - "Class": 0, - "Cooldown": 300, - "Item": { - "Id": 6, - "Name": "Firewall", - "Price": 100, - "MaxAllowed": 1, - "Attributes": { - "CanBuy" : true, - "CanSell" : true, - "CanConsume" : false, - "CanTrade" : false, - "CanDrop" : false - } - } - } - ] - }, - { - "Case": "Shield", - "Fields": [ - { - "Class": 1, - "Cooldown": 300, - "Item": { - "Id": 7, - "Name": "Encryption", - "Price": 100, - "MaxAllowed": 1, - "Attributes": { - "CanBuy" : true, - "CanSell" : true, - "CanConsume" : false, - "CanTrade" : false, - "CanDrop" : false - } - } - } - ] - }, - { - "Case": "Shield", - "Fields": [ - { - "Class": 2, - "Cooldown": 300, - "Item": { - "Id": 8, - "Name": "Cypher", - "Price": 100, - "MaxAllowed": 1, - "Attributes": { - "CanBuy" : true, - "CanSell" : true, - "CanConsume" : false, - "CanTrade" : false, - "CanDrop" : false - } - } - } - ] - }, - { - "Case": "Food", - "Fields": [ - { - "TargetStat" : 0, - "BoostAmount" : 30, - "Item": { - "Id": 12, - "Name": "Protein Powder", - "Price": 50, - "MaxAllowed": 1, - "Attributes": { - "CanBuy" : true, - "CanSell" : false, - "CanConsume" : true, - "CanTrade" : false, - "CanDrop" : false - } - } - } - ] - }, - { - "Case": "Food", - "Fields": [ - { - "TargetStat" : 1, - "BoostAmount" : 30, - "Item": { - "Id": 13, - "Name": "Toro Loco", - "Price": 50, - "MaxAllowed": 1, - "Attributes": { - "CanBuy" : true, - "CanSell" : false, - "CanConsume" : true, - "CanTrade" : false, - "CanDrop" : false - } - } - } - ] - }, - { - "Case": "Food", - "Fields": [ - { - "TargetStat" : 2, - "BoostAmount" : 30, - "Item": { - "Id": 14, - "Name": "Oldports Cigs", - "Price": 50, - "MaxAllowed": 1, - "Attributes": { - "CanBuy" : true, - "CanSell" : false, - "CanConsume" : true, - "CanTrade" : false, - "CanDrop" : false - } - } - } - ] - }, - { - "Case": "Food", - "Fields": [ - { - "TargetStat" : 3, - "BoostAmount" : 30, - "Item": { - "Id": 15, - "Name": "Moon Pie", - "Price": 50, - "MaxAllowed": 1, - "Attributes": { - "CanBuy" : true, - "CanSell" : false, - "CanConsume" : true, - "CanTrade" : false, - "CanDrop" : false - } - } - } - ] - }, - { - "Case": "Accessory", - "Fields": [ - { - "TargetStat" : 0, - "FloorBoost" : 25, - "CeilBoost" : 0, - "Item": { - "Id": 20, - "Name": "Kettlebell", - "MaxAllowed": 1, - "Price": 250, - "Attributes": { - "CanBuy" : true, - "CanSell" : true, - "CanConsume" : false, - "CanTrade" : false, - "CanDrop" : false - } - } - } - ] - }, - { - "Case": "Accessory", - "Fields": [ - { - "TargetStat" : 1, - "FloorBoost" : 25, - "CeilBoost" : 0, - "Item": { - "Id": 21, - "Name": "Headphones", - "MaxAllowed": 1, - "Price": 250, - "Attributes": { - "CanBuy" : true, - "CanSell" : true, - "CanConsume" : false, - "CanTrade" : false, - "CanDrop" : false - } - } - } - ] - }, - { - "Case": "Accessory", - "Fields": [ - { - "TargetStat" : 2, - "FloorBoost" : 0, - "CeilBoost" : 25, - "Item": { - "Id": 22, - "Name": "Rolox Watch", - "MaxAllowed": 1, - "Price": 250, - "Attributes": { - "CanBuy" : true, - "CanSell" : true, - "CanConsume" : false, - "CanTrade" : false, - "CanDrop" : false - } - } - } - ] - }, - { - "Case": "Accessory", - "Fields": [ - { - "TargetStat" : 3, - "FloorBoost" : 0, - "CeilBoost" : 25, - "Item": { - "Id": 23, - "Name": "Buddha Keychain", - "MaxAllowed": 1, - "Price": 250, - "Attributes": { - "CanBuy" : true, - "CanSell" : true, - "CanConsume" : false, - "CanTrade" : false, - "CanDrop" : false - } - } - } - ] - } -] - diff --git a/paket.dependencies b/paket.dependencies index 56f38d1..d60c377 100644 --- a/paket.dependencies +++ b/paket.dependencies @@ -5,9 +5,9 @@ framework: net6.0, netstandard2.0, netstandard2.1 nuget FSharp.Core >= 6.0.0 -nuget DSharpPlus >= 4.2.0-nightly-01105 -nuget DSharpPlus.Interactivity >= 4.2.0-nightly-01105 -nuget DSharpPlus.SlashCommands >= 4.2.0-nightly-01105 +nuget DSharpPlus >= 4.2.0-nightly-01125 +nuget DSharpPlus.Interactivity >= 4.2.0-nightly-01125 +nuget DSharpPlus.SlashCommands >= 4.2.0-nightly-01125 nuget MongoDB.Driver nuget dotenv.net 3.1.1 diff --git a/paket.lock b/paket.lock index 0b19165..166ee7e 100644 --- a/paket.lock +++ b/paket.lock @@ -8,7 +8,7 @@ NUGET System.Buffers (>= 4.5.1) - restriction: || (&& (== net6.0) (>= net471)) (&& (== net6.0) (< netstandard2.0)) (&& (== net6.0) (< netstandard2.1)) (== netstandard2.0) (&& (== netstandard2.1) (>= net471)) (&& (== netstandard2.1) (< netstandard2.0)) dotenv.net (3.1.1) System.Memory (>= 4.5.4) - restriction: || (&& (== net6.0) (< netstandard2.0)) (&& (== net6.0) (< netstandard2.1)) (== netstandard2.0) (&& (== netstandard2.1) (< netstandard2.0)) - DSharpPlus (4.2.0-nightly-01105) + DSharpPlus (4.3.0-nightly-01125) Emzi0767.Common (>= 2.6.2) Microsoft.Extensions.Logging.Abstractions (>= 5.0) Newtonsoft.Json (>= 13.0.1) @@ -18,11 +18,11 @@ NUGET System.Net.WebSockets.Client (>= 4.3.2) System.Runtime.InteropServices.RuntimeInformation (>= 4.3) System.Threading.Channels (>= 5.0) - DSharpPlus.Interactivity (4.2.0-nightly-01105) + DSharpPlus.Interactivity (4.3.0-nightly-01125) ConcurrentHashSet (>= 1.1) - DSharpPlus (>= 4.2.0-nightly-01105) - DSharpPlus.SlashCommands (4.2.0-nightly-01105) - DSharpPlus (>= 4.2.0-nightly-01105) + DSharpPlus (>= 4.3.0-nightly-01125) + DSharpPlus.SlashCommands (4.3.0-nightly-01125) + DSharpPlus (>= 4.3.0-nightly-01125) Microsoft.Extensions.DependencyInjection (>= 5.0.1) Emzi0767.Common (2.6.2) System.Collections.Immutable (>= 5.0) From d47eb1a3ca58ed93a7c679c91279a7a7a719fd34 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 29 Apr 2022 11:39:18 +0700 Subject: [PATCH 08/10] WIP metadata --- Bot/DbService.fs | 7 ++++--- Bot/GameTypes.fs | 1 + Bot/Games/Store.fs | 3 ++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Bot/DbService.fs b/Bot/DbService.fs index 6295aad..c82327d 100644 --- a/Bot/DbService.fs +++ b/Bot/DbService.fs @@ -23,6 +23,7 @@ let readItem (reader : RowReader) = { TargetStat = stat ; Effect = fx } { Item.Id = reader.int "id" Item.Name = reader.string "name" + Item.Description = reader.string "description" Item.IconUrl = reader.string "icon" Item.Symbol = reader.string "symbol" Item.Type = @@ -56,7 +57,7 @@ let getPlayerInventory (did : uint64) = |> Sql.connect |> Sql.parameters [ "did", Sql.string (string did) ] |> Sql.query """ - SELECT ii.id,ii.symbol,name,icon,category,buy_price,sell_price,rate_limit,expiration,drop_chance,can_trade,can_consume, + SELECT ii.id,ii.symbol,name,description,icon,category,buy_price,sell_price,rate_limit,expiration,drop_chance,can_trade,can_consume, attack_power,defense_power,class_name,max_stack,mods FROM inventory_item JOIN item ii on inventory_item.item_id = ii.id @@ -92,7 +93,7 @@ let getStoreItems (channelId : uint64) = |> Sql.connect |> Sql.parameters [ "cid", Sql.string (string channelId) ] |> Sql.query """ - SELECT i.id,i.symbol,name,icon,category,buy_price,sell_price,rate_limit,expiration,drop_chance,can_trade,can_consume, + SELECT i.id,i.symbol,name,description,icon,category,buy_price,sell_price,rate_limit,expiration,drop_chance,can_trade,can_consume, attack_power,defense_power,class_name,max_stack,mods FROM store_item JOIN store st on store_item.store_id = st.id @@ -106,7 +107,7 @@ let getWeapons () = connStr |> Sql.connect |> Sql.query """ - SELECT i.id,i.symbol,name,icon,category,buy_price,sell_price,rate_limit,expiration,drop_chance,can_trade,can_consume, + SELECT i.id,i.symbol,name,description,icon,category,buy_price,sell_price,rate_limit,expiration,drop_chance,can_trade,can_consume, attack_power,defense_power,class_name,max_stack,mods FROM item WHERE category = 'Hack' OR symbol = 'Shield' """ diff --git a/Bot/GameTypes.fs b/Bot/GameTypes.fs index 6feb097..8b8e73b 100644 --- a/Bot/GameTypes.fs +++ b/Bot/GameTypes.fs @@ -132,6 +132,7 @@ let (|CanStack|_|) itemAttrs = itemAttrs |> List.tryPick (function Stackable type Item = { Id : int Name : string + Description : string Type : ItemType Symbol : string IconUrl : string diff --git a/Bot/Games/Store.fs b/Bot/Games/Store.fs index 9e2d621..1516be9 100644 --- a/Bot/Games/Store.fs +++ b/Bot/Games/Store.fs @@ -162,8 +162,9 @@ let purchaseItemEmbed (item : Item) = embed.Title <- $"Purchased {item.Name}" match item.Type with | ItemType.Jpeg -> - if item.Symbol = "RAFFLE" then + if item.Symbol.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}" From d95264743b83034f7c305a77870b2ece0864e4c9 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 29 Apr 2022 16:52:32 +0700 Subject: [PATCH 09/10] WIP stock stuff --- Bot/DbService.fs | 9 +++++++-- Bot/GameTypes.fs | 7 +++++++ Bot/Games/Store.fs | 46 +++++++++++++++++++++++++++------------------- 3 files changed, 41 insertions(+), 21 deletions(-) diff --git a/Bot/DbService.fs b/Bot/DbService.fs index c82327d..baa7c76 100644 --- a/Bot/DbService.fs +++ b/Bot/DbService.fs @@ -93,14 +93,19 @@ let getStoreItems (channelId : uint64) = |> Sql.connect |> Sql.parameters [ "cid", Sql.string (string channelId) ] |> Sql.query """ - SELECT i.id,i.symbol,name,description,icon,category,buy_price,sell_price,rate_limit,expiration,drop_chance,can_trade,can_consume, + SELECT stock,available,limit_stock,i.id,i.symbol,name,description,icon,category,buy_price,sell_price,rate_limit,expiration,drop_chance,can_trade,can_consume, attack_power,defense_power,class_name,max_stack,mods FROM store_item JOIN store st on store_item.store_id = st.id JOIN item i on store_item.item_id = i.id WHERE channel_id = @cid AND available; """ - |> Sql.executeAsync readItem + |> Sql.executeAsync (fun reader -> { + Stock = reader.int "stock" + LimitStock = reader.bool "limit_stock" + Available = reader.bool "available" + StoreItem.Item = readItem reader + }) |> Async.AwaitTask let getWeapons () = diff --git a/Bot/GameTypes.fs b/Bot/GameTypes.fs index 8b8e73b..2bf556d 100644 --- a/Bot/GameTypes.fs +++ b/Bot/GameTypes.fs @@ -139,6 +139,13 @@ type Item = { Attributes : ItemAttribute list } +type StoreItem = { + Stock : int + LimitStock : bool + Available : bool + Item : Item +} + type HackItem = { Id : int Name : string diff --git a/Bot/Games/Store.fs b/Bot/Games/Store.fs index 1516be9..f531ed1 100644 --- a/Bot/Games/Store.fs +++ b/Bot/Games/Store.fs @@ -10,20 +10,22 @@ open Degenz open Degenz.Messaging open Degenz.PlayerInteractions -let getItemEmbeds owned (items : Inventory) = +let getItemEmbeds owned (items : StoreItem list) = items - |> List.countBy (fun item -> item.Id) - |> List.map (fun (id,count) -> items |> List.find (fun i -> i.Id = id) , count ) + |> 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() - item.Attributes + if not owned && item.LimitStock then + embed.AddField("Stock", $"{item.Stock}", true) |> ignore + item.Item.Attributes |> List.iter (function | Buyable price -> embed.AddField("Price πŸ’°", (if price = 0 then "Free" else $"{price} $GBT"), true) |> ignore | Attackable power -> - let title = match item.Type with ItemType.Hack -> "$GBT Reward" | _ -> "Power" + let title = match item.Item.Type with ItemType.Hack -> "$GBT Reward" | _ -> "Power" embed.AddField($"{title} |", string power, true) |> ignore | RateLimitable time -> - let title = match item.Type with ItemType.Hack -> "Cooldown" | ItemType.Shield -> "Active For" | _ -> "Expires" + 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" embed.AddField($"{title} |", timeStr, true) |> ignore @@ -47,23 +49,23 @@ let getItemEmbeds owned (items : Inventory) = embed.AddField($"Effect - Amount ", $"{fx}", true) |> ignore | _ -> ()) embed - .WithColor(WeaponClass.getClassEmbedColor item) - .WithTitle($"{item.Name}") + .WithColor(WeaponClass.getClassEmbedColor item.Item) + .WithTitle($"{item.Item.Name}") |> ignore - match Embeds.getItemIcon item.Id with + match Embeds.getItemIcon item.Item.Id with | Some url -> embed.WithThumbnail(url) - | None -> if String.IsNullOrWhiteSpace(item.IconUrl) then embed else embed.WithThumbnail(item.IconUrl)) + | None -> if String.IsNullOrWhiteSpace(item.Item.IconUrl) then embed else embed.WithThumbnail(item.Item.IconUrl)) |> List.map (fun e -> e.Build()) |> Seq.ofList -let getBuyItemsEmbed (playerInventory : Inventory) (storeInventory : Inventory) = +let getBuyItemsEmbed (playerInventory : Inventory) (storeInventory : StoreItem list) = let embeds = getItemEmbeds false storeInventory let buttons = storeInventory |> List.map (fun item -> - if playerInventory |> List.exists (fun i -> i.Id = item.Id) - then DiscordButtonComponent(WeaponClass.getClassButtonColor item, $"Buy-{item.Id}", $"Own {item.Name}", true) - else DiscordButtonComponent(WeaponClass.getClassButtonColor item, $"Buy-{item.Id}", $"Buy {item.Name}") + if playerInventory |> List.exists (fun i -> i.Id = item.Item.Id) + then DiscordButtonComponent(WeaponClass.getClassButtonColor item.Item, $"Buy-{item.Item.Id}", $"Own {item.Item.Name}", true) + else DiscordButtonComponent(WeaponClass.getClassButtonColor item.Item, $"Buy-{item.Item.Id}", $"Buy {item.Item.Name}") :> DiscordComponent) let builder = @@ -174,7 +176,8 @@ let purchaseItemEmbed (item : Item) = let handleBuyItem (ctx : IDiscordContext) itemId = executePlayerAction ctx (fun player -> async { let! storeInventory = DbService.getStoreItems (ctx.GetChannel().Id) - let item = storeInventory |> Inventory.findItemById itemId +// let item = storeInventory |> Inventory.findItemById itemId |> fun i -> { Item = i ; Stock = 1 ; LimitStock = false ; Available = true } + let item = storeInventory |> List.map (fun i -> i.Item) |> Inventory.findItemById itemId do! player |> checkHasSufficientFunds item >>= checkDoesntExceedStackCap item @@ -218,8 +221,9 @@ let handleSell (ctx : IDiscordContext) itemId = 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 -> - let embeds = getItemEmbeds true items + | items -> + let items' = items |> List.map (fun i -> { 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 }) @@ -229,13 +233,17 @@ let handleConsume (ctx : IDiscordContext) itemId = PlayerInteractions.executePla match player.Inventory |> Inventory.getFoods with | [] -> do! Messaging.sendFollowUpMessage ctx "You do not have any items to consume" | items -> - let embeds = getItemEmbeds true items + let items' = items |> List.map (fun i -> { 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 showJpegsEmbed (ctx : IDiscordContext) = PlayerInteractions.executePlayerAction ctx (fun player -> async { - let jpegs = player.Inventory |> Inventory.getItemsByType ItemType.Jpeg + let jpegs = + player.Inventory + |> Inventory.getItemsByType ItemType.Jpeg + |> List.map (fun i -> { 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 From d99b7d6f2f5f6411a44818542612c2a91cbad37f Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sun, 1 May 2022 12:10:29 +0700 Subject: [PATCH 10/10] WIP stock --- Bot/DbService.fs | 12 ++++++++++++ Bot/GameHelpers.fs | 11 +++++------ Bot/Games/Store.fs | 19 ++++++++++++++----- 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/Bot/DbService.fs b/Bot/DbService.fs index baa7c76..dec6bb4 100644 --- a/Bot/DbService.fs +++ b/Bot/DbService.fs @@ -108,6 +108,18 @@ let getStoreItems (channelId : uint64) = }) |> Async.AwaitTask +let decrementItemStock (item : Item) = + connStr + |> Sql.connect + |> Sql.parameters [ ( "iid" , Sql.int item.Id) ] + |> Sql.query """ + UPDATE store_item SET stock = GREATEST(stock - 1, 0) + WHERE store_item.item_id = @iid + """ + |> Sql.executeNonQueryAsync + |> Async.AwaitTask + |> Async.Ignore + let getWeapons () = connStr |> Sql.connect diff --git a/Bot/GameHelpers.fs b/Bot/GameHelpers.fs index fc1f0d8..3cb66d1 100644 --- a/Bot/GameHelpers.fs +++ b/Bot/GameHelpers.fs @@ -4,17 +4,16 @@ open System open DSharpPlus open DSharpPlus.Entities open Degenz -open Newtonsoft.Json module Inventory = let getItemsByType itemType inventory = match itemType with - | ItemType.Hack -> inventory |> List.filter (fun item -> match item.Type with ItemType.Hack _ -> true | _ -> false) - | ItemType.Shield -> inventory |> List.filter (fun item -> match item.Type with ItemType.Shield _ -> true | _ -> false) - | ItemType.Food -> inventory |> List.filter (fun item -> match item.Type with ItemType.Food _ -> true | _ -> false) + | ItemType.Hack -> inventory |> List.filter (fun item -> match item.Type with ItemType.Hack _ -> true | _ -> false) + | ItemType.Shield -> inventory |> List.filter (fun item -> match item.Type with ItemType.Shield _ -> true | _ -> false) + | ItemType.Food -> inventory |> List.filter (fun item -> match item.Type with ItemType.Food _ -> true | _ -> false) | ItemType.Accessory -> inventory |> List.filter (fun item -> match item.Type with ItemType.Accessory _ -> true | _ -> false) - | ItemType.Jpeg -> inventory |> List.filter (fun item -> match item.Type with ItemType.Jpeg _ -> true | _ -> false) - | ItemType.Misc -> inventory |> List.filter (fun item -> match item.Type with ItemType.Misc _ -> true | _ -> false) + | ItemType.Jpeg -> inventory |> List.filter (fun item -> match item.Type with ItemType.Jpeg _ -> true | _ -> false) + | ItemType.Misc -> inventory |> List.filter (fun item -> match item.Type with ItemType.Misc _ -> true | _ -> false) let findItemById id (inventory : Inventory) = inventory |> List.find (fun item -> item.Id = id) diff --git a/Bot/Games/Store.fs b/Bot/Games/Store.fs index f531ed1..b16ef58 100644 --- a/Bot/Games/Store.fs +++ b/Bot/Games/Store.fs @@ -63,9 +63,12 @@ let getBuyItemsEmbed (playerInventory : Inventory) (storeInventory : StoreItem l let buttons = storeInventory |> List.map (fun item -> - if playerInventory |> List.exists (fun i -> i.Id = item.Item.Id) - then DiscordButtonComponent(WeaponClass.getClassButtonColor item.Item, $"Buy-{item.Item.Id}", $"Own {item.Item.Name}", true) - else DiscordButtonComponent(WeaponClass.getClassButtonColor item.Item, $"Buy-{item.Item.Id}", $"Buy {item.Item.Name}") + 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}", $"Buy {item.Item.Name}") + | false , false -> DiscordButtonComponent(WeaponClass.getClassButtonColor item.Item, $"Buy-{item.Item.Id}", $"{item.Item.Name} (Out of Stock)", true) + | true , _ -> DiscordButtonComponent(WeaponClass.getClassButtonColor item.Item, $"Buy-{item.Item.Id}", $"Own {item.Item.Name}", true) :> DiscordComponent) let builder = @@ -101,6 +104,11 @@ let getSellEmbed (items : Inventory) = .AddComponents(buttons) .AsEphemeral(true) +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 -> @@ -176,16 +184,17 @@ let purchaseItemEmbed (item : Item) = let handleBuyItem (ctx : IDiscordContext) itemId = executePlayerAction ctx (fun player -> async { let! storeInventory = DbService.getStoreItems (ctx.GetChannel().Id) -// let item = storeInventory |> Inventory.findItemById itemId |> fun i -> { Item = i ; Stock = 1 ; LimitStock = false ; Available = true } + 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 do! DbService.updatePlayerCurrency -price player |> Async.Ignore do! DbService.addToPlayerInventory player.DiscordId item |> Async.Ignore -// do! ctx.FollowUp $"Successfully purchased {item.Name}! You now have {player.Bank - price} πŸ’°$GBT remaining" + do! DbService.decrementItemStock item let builder = DiscordFollowupMessageBuilder().AsEphemeral(true) builder.AddEmbed(purchaseItemEmbed (item)) |> ignore do! ctx.FollowUp builder |> Async.AwaitTask