From f26d51701d1f1a1ba7c94d6a1bfc1ed1c703045f Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Thu, 28 Apr 2022 17:43:50 +0700 Subject: [PATCH] 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