diff --git a/Bot/Admin.fs b/Bot/Admin.fs index dbead26..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.setWhitelistStock (int amount)) + 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 ebd95de..1a19b85 100644 --- a/Bot/Analytics.fs +++ b/Bot/Analytics.fs @@ -94,33 +94,33 @@ 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) (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/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 6c99a5e..dec6bb4 100644 --- a/Bot/DbService.fs +++ b/Bot/DbService.fs @@ -1,11 +1,149 @@ 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.Description = reader.string "description" + 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)) + 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,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 + 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 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 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 (fun reader -> { + Stock = reader.int "stock" + LimitStock = reader.bool "limit_stock" + Available = reader.bool "available" + StoreItem.Item = readItem reader + }) + |> 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 + |> Sql.query """ + 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' + """ + |> 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 @@ -34,36 +172,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 @@ -89,28 +197,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 @@ -135,7 +240,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 @@ -146,14 +251,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 @@ -283,40 +386,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/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 ca6e930..3cb66d1 100644 --- a/Bot/GameHelpers.fs +++ b/Bot/GameHelpers.fs @@ -4,64 +4,78 @@ open System open DSharpPlus open DSharpPlus.Entities open Degenz -open Newtonsoft.Json - -module Armory = - let weapons : ItemDetails list = - let file = System.IO.File.ReadAllText("Items.json") -// let file = System.IO.File.ReadAllText("Bot/Items.json") - JsonConvert.DeserializeObject(file) - |> Array.toList 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) + | 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) 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 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 = +// 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 getHackItem + let getShields inventory = inventory |> List.choose getShieldItem let getFoods inventory = - inventory |> List.choose (fun item -> match item with | Food f -> Some f | _ -> None) + inventory |> List.choose (fun item -> match item.Type with | ItemType.Food -> Some item | _ -> None) let getAccessories inventory = - inventory |> List.choose (fun item -> match item with | Accessory a -> Some a | _ -> None) + 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 = @@ -82,32 +96,48 @@ 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 ; 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.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.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 ", " - let actionFormat (actions : PlayerEvent List) = + let actionFormat items (actions : PlayerEvent List) = match actions with | [] -> "None" | acts -> @@ -115,13 +145,13 @@ module Arsenal = |> List.map (fun event -> match event.Type with | Hacking h -> - let item = Armory.weapons |> Inventory.findHackById h.HackId + let item = items |> 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 = items |> 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" @@ -130,5 +160,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 5f04ce7..2bf556d 100644 --- a/Bot/GameTypes.fs +++ b/Bot/GameTypes.fs @@ -2,7 +2,6 @@ module Degenz.Types open System -open Degenz [] type mins @@ -89,84 +88,83 @@ type ItemType = | Shield | Food | Accessory - -type ItemAttributes = { - CanBuy : bool - CanSell : bool - CanConsume : bool - CanTrade : bool - CanDrop : bool + | Jpeg + | Misc + +type Effect = + | Min of int + | Max of int + | Add of int + | RateMultiplier of float + +type StatEffect = { + TargetStat : StatId + Effect : Effect } +type ItemAttribute = + | Buyable of price : int + | Sellable of price : int + | RateLimitable of cooldown : int + | Expireable of lifetime : int + | Consumable + | Modifiable of effects : StatEffect list + | Droppable of chance : float + | 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 (|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 - Price : int - Attributes : ItemAttributes + Description : string + Type : ItemType + Symbol : string + IconUrl : string + Attributes : ItemAttribute list +} + +type StoreItem = { + Stock : int + LimitStock : bool + Available : bool + Item : Item } type HackItem = { - Power : int - Class : int + Id : int + Name : string + Price : int Cooldown : int - Item : Item + Power : int + Class : string } type ShieldItem = { - Class : int + Id : int + Name : string + Price : int Cooldown : int - Item : Item + Resistance : int + Class : string } -type FoodItem = { - TargetStat : StatId - BoostAmount : int - Item : Item -} - -type AccessoryItem = { - TargetStat : StatId - FloorBoost : int - CeilBoost : int - 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 @@ -190,3 +188,4 @@ with member this.toDiscordPlayer = { Id = this.DiscordId ; Name = this.Name } // XP = 0 Bank = 0 Active = false } + diff --git a/Bot/Games/HackerBattle.fs b/Bot/Games/HackerBattle.fs index 18745bf..9374bbc 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 Trainer.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,27 @@ 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 + // 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 - 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 +207,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 = Trainer.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 +230,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/SlotMachine.fs b/Bot/Games/SlotMachine.fs index 1003aa9..15e4499 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" @@ -152,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 @@ -160,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 @@ -168,10 +150,43 @@ 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 +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 +336,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..b16ef58 100644 --- a/Bot/Games/Store.fs +++ b/Bot/Games/Store.fs @@ -10,97 +10,128 @@ open Degenz open Degenz.Messaging open Degenz.PlayerInteractions -let getBuyItemsEmbed (playerInventory : Inventory) (storeInventory : Inventory) = - let embeds , buttons = +let getItemEmbeds owned (items : StoreItem list) = + items + |> List.countBy (fun item -> item.Item.Id) + |> List.map (fun (id,count) -> items |> List.find (fun i -> i.Item.Id = id) , count ) + |> List.map (fun (item,count) -> + let embed = DiscordEmbedBuilder() + 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.Item.Type with ItemType.Hack -> "$GBT Reward" | _ -> "Power" + embed.AddField($"{title} |", string power, true) |> ignore + | RateLimitable time -> + 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 + | 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 -> $"{f.TargetStat} Min + {i}" + | Max i -> $"{f.TargetStat} Max + {i}" + | Add i -> + let str = if i > 0 then "Boost" else "Penalty" + $"{f.TargetStat} {str} + {i}" + | RateMultiplier i -> $"{f.TargetStat} Multiplier - i") + |> String.concat "\n" + embed.AddField($"Effect - Amount ", $"{fx}", true) |> ignore + | _ -> ()) + embed + .WithColor(WeaponClass.getClassEmbedColor item.Item) + .WithTitle($"{item.Item.Name}") + |> ignore + match Embeds.getItemIcon item.Item.Id with + | Some url -> embed.WithThumbnail(url) + | 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 : StoreItem list) = + let embeds = getItemEmbeds false storeInventory + let buttons = 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 - embed - .AddField("Price 💰", (if item.Price = 0 then "Free" else $"{item.Price} $GBT"), true) - .WithColor(WeaponClass.getClassEmbedColor item) - .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 + 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) - 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 : 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.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 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.unzip - - DiscordFollowupMessageBuilder() - .AddEmbeds(embeds) - .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 = - 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) - 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." @@ -110,13 +141,19 @@ 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 - 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 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 + 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}" }) let sell itemType getItems (ctx : IDiscordContext) = @@ -126,47 +163,123 @@ 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.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}" + embed + // 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 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.getItem - >>= checkAlreadyOwnsItem item.getItem + |> checkHasSufficientFunds item + >>= checkHasStock storeItem + >>= checkDoesntExceedStackCap item |> handleResultWithResponse ctx (fun player -> async { - let newBalance = player.Bank - item.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 + 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! DbService.decrementItemStock item + 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.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 + | _ -> () }) }) +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 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 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 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 + |> 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 +}) + +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 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 +}) + let handleStoreEvents (_ : DiscordClient) (event : ComponentInteractionCreateEventArgs) = let ctx = DiscordEventContext event :> IDiscordContext let id = ctx.GetInteractionId() @@ -174,6 +287,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() @@ -181,7 +296,28 @@ let handleStoreEvents (_ : DiscordClient) (event : ComponentInteractionCreateEve builder.Content <- $"Incorrect Action identifier {id}" 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 () @@ -194,32 +330,30 @@ type Store() = do! Messaging.sendSimpleResponse ctx msg } - let checkChannel (ctx : IDiscordContext) = - match ctx.GetChannel().Id with -// | id when id = GuildEnvironment.channelBackAlley -> buy (Inventory.getItemsByType 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 - | _ -> - task { - let msg = $"This channel doesn't have any items to sell" - 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 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) = buy (DiscordInteractionContext ctx) -// [] -// member _.BuyItem (ctx : InteractionContext) = checkChannel (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.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)) @@ -227,10 +361,13 @@ 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 "Shields" (Inventory.getItemsByType ItemType.Food)) + member this.Consume (ctx : InteractionContext) = consume (DiscordInteractionContext ctx) -// [] -// member this.Inventory (ctx : InteractionContext) = -// enforceChannel (DiscordInteractionContext(ctx)) (sell "Shields" (Inventory.getItemsByType ItemType)) + [] + member this.Inventory (ctx : InteractionContext) = + showJpegsEmbed (DiscordInteractionContext ctx) + + [] + member this.Stats (ctx : InteractionContext) = + showStats (DiscordInteractionContext ctx) diff --git a/Bot/Games/Trainer.fs b/Bot/Games/Trainer.fs index 4549a86..7a32086 100644 --- a/Bot/Games/Trainer.fs +++ b/Bot/Games/Trainer.fs @@ -9,8 +9,11 @@ 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 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 let BeginnerProtectionHours = 24 @@ -21,7 +24,7 @@ let HackEvent () = { Adversary = Sensei Success = true IsInstigator = true - HackId = defaultHack.Item.Id + HackId = defaultHack.Id } } let ShieldEvents () = [ @@ -63,7 +66,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 +81,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 +104,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 +120,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 +133,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 +169,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 +197,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 +212,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 +230,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 55641b3..072f4a0 100644 --- a/Bot/GuildEnvironment.fs +++ b/Bot/GuildEnvironment.fs @@ -35,9 +35,9 @@ let channelWhitelist = getId "CHANNEL_WHITELIST" let channelShelters = getId "CHANNEL_SHELTERS" let channelSlots = getId "CHANNEL_SLOTS" //let channelJackpotNum = getId "CHANNEL_JACKPOTNUM" -//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" @@ -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 diff --git a/Bot/InviteTracker.fs b/Bot/InviteTracker.fs index 85350bd..68f7a2f 100644 --- a/Bot/InviteTracker.fs +++ b/Bot/InviteTracker.fs @@ -178,6 +178,48 @@ let getInvitedUserCount userId = |> Sql.executeRowAsync (fun read -> read.int "count") |> Async.AwaitTask +let getWhitelistItem () = + connStr + |> Sql.connect + |> Sql.query """ + 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 "buy_price") * 1 |}) + |> Async.AwaitTask + +let updateWhitelistStock () = async { + try + do! connStr + |> Sql.connect + |> Sql.query """ + 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 + |> 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 store_item SET stock = @amount + WHERE store_item.item_id = (SELECT id FROM item 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 +473,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 +522,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 +541,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 +630,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 diff --git a/Bot/Items.json b/Bot/Items.json deleted file mode 100644 index 959d33a..0000000 --- a/Bot/Items.json +++ /dev/null @@ -1,304 +0,0 @@ -[ - { - "Case": "Hack", - "Fields": [ - { - "Power": 20, - "Class": 0, - "Cooldown": 2, - "Item": { - "Id": 0, - "Name": "Virus", - "Price": 250, - "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, - "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, - "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, - "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, - "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, - "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, - "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, - "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, - "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, - "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", - "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", - "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", - "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", - "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)