From bf68d0ae4abdfec12f05484f05b8bb8bbfdbc4df Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Wed, 27 Apr 2022 09:53:27 +0700 Subject: [PATCH] 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 =