diff --git a/Bot/Bot.fs b/Bot/Bot.fs index 8f82a14..521c553 100644 --- a/Bot/Bot.fs +++ b/Bot/Bot.fs @@ -44,7 +44,7 @@ let storeCommands = storeBot.UseSlashCommands() hackerCommands.RegisterCommands(guild); hackerCommands.RegisterCommands(guild); -hackerCommands.RegisterCommands(guild); +//hackerCommands.RegisterCommands(guild); storeCommands.RegisterCommands(guild); //sc3.RegisterCommands(guild); diff --git a/Bot/Embeds.fs b/Bot/Embeds.fs index f3a0524..bac72ae 100644 --- a/Bot/Embeds.fs +++ b/Bot/Embeds.fs @@ -165,7 +165,7 @@ let getArsenalEmbed (player : PlayerData) = DiscordEmbedBuilder() .AddField( "Arsenal", Arsenal.statusFormat player )) -let getAchievementEmbed description achievement = +let getAchievementEmbed rewards description achievement = let embed = DiscordEmbedBuilder() GuildEnvironment.botUserHackerBattle @@ -176,12 +176,12 @@ let getAchievementEmbed description achievement = DiscordFollowupMessageBuilder() .AddEmbed( - // TODO: We can add a Reward field but we'd need to keep track of what the player was awarded embed.WithTitle("Achievement Unlocked!") .WithDescription(description) .WithColor(DiscordColor.Gold) // .AddField("Achievement", $"🏆 {achievement}") - .AddField("Achievement", $"{achievement}") + .AddField("Achievement", $"{achievement}", true) + .AddField("Rewards", rewards |> String.concat "\n", true) // TODO: Once we add another achievement, fix this .WithImageUrl("https://s10.gifyu.com/images/MasterTraining_Degenz.gif")) .AsEphemeral(true) \ No newline at end of file diff --git a/Bot/Game.fs b/Bot/Game.fs index 63600a9..61fc7b5 100644 --- a/Bot/Game.fs +++ b/Bot/Game.fs @@ -73,26 +73,12 @@ module Player = player.Events |> Array.filter (fun act -> match act.Type with PlayerEventType.Shielding -> true | _ -> false && act.ItemId < 12) - // TODO: This parameter is a result of putting the cooldown on the attack side. Put the cooldown on the defender - // side and only check if it's the same target, we need to refactor Actions - let removeExpiredActions filterByAttackCooldown player = + let removeExpiredActions player = let actions = player.Events |> Array.filter (fun (act : PlayerEvent) -> - let itemCooldown = - if act.ItemId > 0 && act.ItemId < 12 then - (Armory.getItem act.ItemId).Cooldown - else - match act.Type with - | PlayerEventType.Steal -> 1 - | _ -> 720 - |> int - - match act.Type , filterByAttackCooldown with - | PlayerEventType.Hacking , true -> System.DateTime.UtcNow - act.Timestamp < System.TimeSpan.FromMinutes(itemCooldown) - | PlayerEventType.Hacking , false -> System.DateTime.UtcNow - act.Timestamp < Game.SameTargetAttackCooldown - | PlayerEventType.Shielding , _ -> System.DateTime.UtcNow - act.Timestamp < System.TimeSpan.FromMinutes(itemCooldown) - | _ -> System.DateTime.UtcNow - act.Timestamp < System.TimeSpan.FromMinutes(itemCooldown)) + let cooldown = System.TimeSpan.FromMinutes(int act.Cooldown) + System.DateTime.UtcNow - act.Timestamp < cooldown) { player with Events = actions } let modifyBank (player : PlayerData) amount = { player with Bank = max (player.Bank + amount) 0 } diff --git a/Bot/HackerBattle.fs b/Bot/HackerBattle.fs index d366cab..8509f98 100644 --- a/Bot/HackerBattle.fs +++ b/Bot/HackerBattle.fs @@ -14,37 +14,30 @@ let checkPlayerIsAttackingThemselves defender attacker = | true -> Error "You think you're clever? You can't hack yourself, pal." | false -> Ok attacker -let checkAlreadyHackedTarget defenderId attacker = - attacker.Events - |> Array.tryFind (fun pe -> pe.Adversary.Id = defenderId) +let checkAlreadyHackedTarget defender attacker = + defender.Events + |> Array.tryFind (fun event -> event.Adversary.Id = attacker.DiscordId && event.IsInstigator = false) |> function | Some event -> let cooldown = getTimeText true Game.SameTargetAttackCooldown event.Timestamp - Error $"You can only hack the same target once every {Game.SameTargetAttackCooldown.Hours} hours, wait {cooldown} to attempt another hack on {event.Adversary.Name}." + Error $"You can only hack the same target once every {Game.SameTargetAttackCooldown.Hours} hours, wait {cooldown} to attempt another hack on <@{defender.DiscordId}>." | None -> Ok attacker -let checkItemHasCooldown itemId attacker = - let cooldown = - attacker.Events - |> Array.tryFind (fun a -> a.ItemId = itemId) - |> function - | Some a -> a.Timestamp - | None -> DateTime.MinValue - let item = Armory.getItem itemId - if DateTime.UtcNow - cooldown > TimeSpan.FromMinutes(int item.Cooldown) then - Ok attacker - else - let cooldown = getTimeText true (TimeSpan.FromMinutes(int item.Cooldown)) cooldown - let item = Armory.battleItems |> Array.find (fun i -> i.Id = itemId) - Error $"{item.Name} is currently on cooldown, wait {cooldown} to use it again." +let checkWeaponHasCooldown (weapon : Item) attacker = + let cooldown = attacker.Events |> Array.tryFind (fun a -> a.ItemId = weapon.Id) + match cooldown with + | Some event -> + let cooldown = getTimeText true (TimeSpan.FromMinutes(int event.Cooldown)) event.Timestamp + Error $"{weapon.Name} is still active, it will expire in {cooldown}." + | None -> Ok attacker let checkHasEmptyHacks attacker = match Player.getHacks attacker with | [||] -> Error $"You currently do not have any Hacks to steal 💰$GBT from others. Please go to the <#{GuildEnvironment.channelArmory}> and purchase one." | _ -> Ok attacker -let checkPlayerOwnsWeapon itemId player = - match player.Inventory |> Array.exists (fun i -> i.Id = itemId) with +let checkPlayerOwnsWeapon (item : Item) player = + match player.Inventory |> Array.exists (fun i -> i.Id = item.Id) with | true -> Ok player | false -> Error $"You sold your weapon already, you cheeky bastard..." @@ -53,8 +46,8 @@ let checkTargetHasMoney (target : PlayerData) attacker = then Error $"{target.Name} does not have enough 💰$GBT to steal from, the broke loser. Pick a different target." else Ok attacker -let checkPlayerHasShieldSlotsAvailable shield player = - let updatedPlayer = player |> Player.removeExpiredActions false +let checkPlayerHasShieldSlotsAvailable (shield : Item) player = + let updatedPlayer = player |> Player.removeExpiredActions let defenses = Player.getShieldEvents updatedPlayer match defenses |> Array.length >= 2 with | true -> @@ -71,37 +64,40 @@ let calculateDamage (hack : Item) (shield : Item) = let runHackerBattle defender hack = defender - |> Player.removeExpiredActions false + |> Player.removeExpiredActions |> Player.getShieldEvents |> Array.map (fun dfn -> Armory.battleItems |> Array.find (fun w -> w.Id = dfn.ItemId)) |> Array.map (calculateDamage (hack)) |> Array.contains Weak -let updateCombatants attacker defender hack prize = +let updateCombatants successfulHack (attacker : PlayerData) (defender : PlayerData) (hack : Item) prize = let updatePlayer amount attack p = { p with Events = Array.append [| attack |] p.Events ; Bank = max (p.Bank + amount) 0 } - let target = { Id = defender.DiscordId ; Name = defender.Name } - let attack = { - ItemId = int hack + let event isDefenderEvent = { + ItemId = hack.Id Type = PlayerEventType.Hacking - Adversary = target - Result = if prize > 0 then PlayerEventResult.Positive else PlayerEventResult.Negative + Adversary = if isDefenderEvent then attacker.basicPlayer else defender.basicPlayer + Cooldown = if isDefenderEvent then Game.SameTargetAttackCooldown.Minutes * 1 else hack.Cooldown Timestamp = DateTime.UtcNow + IsInstigator = not isDefenderEvent + Result = + match successfulHack , isDefenderEvent with + | true , true -> PlayerEventResult.Negative + | false , true -> PlayerEventResult.Positive + | true , false -> PlayerEventResult.Positive + | false , false -> PlayerEventResult.Negative } - // TODO: This is what I was talking about, this isn't a "Shield" event, this is a hack event but there's an adversary - // who loses, so the event itself is to just "hack", so there's no "mugged" event, there's just a failed steal defense - // or something like that. - [ DbService.updatePlayer <| updatePlayer prize attack attacker - DbService.updatePlayer <| Player.modifyBank defender -prize ] + [ DbService.updatePlayer <| updatePlayer prize (event false) attacker + DbService.updatePlayer <| updatePlayer -prize (event true) defender ] |> Async.Parallel |> Async.Ignore let successfulHack (ctx : IDiscordContext) attacker defender hack = async { - do! updateCombatants attacker defender hack Game.HackPrize + do! updateCombatants true attacker defender hack Game.HackPrize - let embed = Embeds.responseSuccessfulHack true defender.DiscordId (Armory.getItem hack) + let embed = Embeds.responseSuccessfulHack true defender.DiscordId hack do! ctx.FollowUp embed |> Async.AwaitTask let builder = Embeds.eventSuccessfulHack ctx defender Game.HackPrize @@ -116,7 +112,7 @@ let failedHack (ctx : IDiscordContext) attacker defender hack = let msg = $"Hack failed! {defender.Name} was able to mount a successful defense! You lost {Game.ShieldPrize} $GBT!" do! sendFollowUpMessage ctx msg - do! updateCombatants attacker defender hack -Game.ShieldPrize + do! updateCombatants false attacker defender hack -Game.ShieldPrize let builder = DiscordMessageBuilder() builder.WithContent($"Hacking attempt failed! <@{defender.DiscordId}> defended hack from {ctx.GetDiscordMember().Username} and stole {Game.ShieldPrize} $GBT from them! ") |> ignore @@ -129,8 +125,8 @@ let failedHack (ctx : IDiscordContext) attacker defender hack = let attack (target : DiscordUser) (ctx : IDiscordContext) = Game.executePlayerActionWithTarget target ctx (fun attacker defender -> async { do! attacker - |> checkAlreadyHackedTarget defender.DiscordId - (Player.removeExpiredActions true) + |> checkAlreadyHackedTarget defender + Player.removeExpiredActions >>= checkHasEmptyHacks >>= checkTargetHasMoney defender >>= checkPlayerIsAttackingThemselves defender @@ -145,23 +141,23 @@ let handleAttack (ctx : IDiscordContext) = Game.executePlayerAction ctx (fun attacker -> async { let split = ctx.GetInteractionId().Split("-") let hackId = int split.[1] - let hack = enum(hackId) - let ( resultId , targetId ) = UInt64.TryParse split.[2] + let hack = Armory.getItem hackId + let resultId , targetId = UInt64.TryParse split.[2] let! resultTarget = DbService.tryFindPlayer targetId match resultTarget , true , resultId with | Some defender , true , true -> do! attacker - |> Player.removeExpiredActions false - |> checkAlreadyHackedTarget defender.DiscordId - >>= checkPlayerOwnsWeapon hackId - >>= checkItemHasCooldown hackId + |> Player.removeExpiredActions + |> checkAlreadyHackedTarget defender + >>= checkPlayerOwnsWeapon hack + >>= checkWeaponHasCooldown hack |> function | Ok atkr -> - runHackerBattle defender (Armory.getItem (int hackId)) + runHackerBattle defender hack |> function - | false -> successfulHack ctx atkr defender hackId - | true -> failedHack ctx attacker defender hackId + | false -> successfulHack ctx atkr defender hack + | true -> failedHack ctx attacker defender hack | Error msg -> Messaging.sendFollowUpMessage ctx msg | _ -> do! Messaging.sendFollowUpMessage ctx "Error occurred processing attack" }) @@ -169,7 +165,7 @@ let handleAttack (ctx : IDiscordContext) = let defend (ctx : IDiscordContext) = Game.executePlayerAction ctx (fun player -> async { if Player.getShields player |> Array.length > 0 then - let p = Player.removeExpiredActions false player + let p = Player.removeExpiredActions player let embed = Embeds.pickDefense "Defend" p false do! ctx.FollowUp embed |> Async.AwaitTask else @@ -184,20 +180,22 @@ let handleDefense (ctx : IDiscordContext) = let shield = Armory.getItem shieldId do! player - |> checkPlayerOwnsWeapon shieldId + |> checkPlayerOwnsWeapon shield >>= checkPlayerHasShieldSlotsAvailable shield - >>= checkItemHasCooldown shieldId - |> handleResultWithResponse ctx (fun _ -> async { // Don't use this player, it removes player cooldowns - let embed = Embeds.responseCreatedShield (Armory.getItem shieldId) + >>= checkWeaponHasCooldown shield + |> handleResultWithResponse ctx (fun p -> async { + let embed = Embeds.responseCreatedShield shield do! ctx.FollowUp embed |> Async.AwaitTask let defense = { ItemId = shieldId Type = PlayerEventType.Shielding Result = PlayerEventResult.Positive Timestamp = DateTime.UtcNow + Cooldown = shield.Cooldown + IsInstigator = true Adversary = DiscordPlayer.empty } - do! DbService.updatePlayer <| { player with Events = Array.append [| defense |] player.Events } + do! DbService.updatePlayer <| { p with Events = Array.append [| defense |] p.Events } let builder = DiscordMessageBuilder() builder.WithContent($"{ctx.GetDiscordMember().Username} has protected their system!") |> ignore let channel = ctx.GetGuild().GetChannel(GuildEnvironment.channelEventsHackerBattle) @@ -209,7 +207,7 @@ let handleDefense (ctx : IDiscordContext) = let arsenal (ctx : IDiscordContext) = Game.executePlayerAction ctx (fun player -> async { - let updatedPlayer = Player.removeExpiredActions false player + let updatedPlayer = Player.removeExpiredActions player let builder = DiscordFollowupMessageBuilder() let embed = DiscordEmbedBuilder() embed.AddField("Arsenal", Arsenal.statusFormat updatedPlayer) |> ignore diff --git a/Bot/Thief.fs b/Bot/Thief.fs index 83975d0..fa67294 100644 --- a/Bot/Thief.fs +++ b/Bot/Thief.fs @@ -92,8 +92,8 @@ let getResultEmbed chance prize (bank : int) thief (victim : DiscordPlayer) let checkVictimStealingCooldown defender attacker = defender - |> Player.removeExpiredActions false - |> Player.getShieldEvents + |> Player.removeExpiredActions + |> fun p -> p.Events |> Array.tryFind (fun pe -> pe.Type = PlayerEventType.Steal && pe.Result = PlayerEventResult.Negative) |> function | Some act -> @@ -102,19 +102,16 @@ let checkVictimStealingCooldown defender attacker = Error $"{defender.Name} was robbed recently so they won't be going out for at least another {hours}." | None -> Ok attacker -// TODO: Look for ways to generalize checking for action cooldowns let checkThiefCooldown attacker = attacker - |> Player.getHackEvents + |> Player.removeExpiredActions + |> fun p -> p.Events |> Array.tryFind (fun pe -> pe.Type = PlayerEventType.Steal) |> function | Some act -> - if ThiefCooldown > (DateTime.UtcNow - act.Timestamp) then - let cooldown = ThiefCooldown - (DateTime.UtcNow - act.Timestamp) - let minutes = if cooldown.Minutes = 0 then "minute" else $"{cooldown.Minutes} minutes" - Error $"Whoa there you clepto, wait at least another {minutes} before you try stealing again." - else - Ok attacker + let cooldown = ThiefCooldown - (DateTime.UtcNow - act.Timestamp) + let minutes = if cooldown.Minutes = 0 then "minute" else $"{cooldown.Minutes} minutes" + Error $"Whoa there you clepto, wait at least another {minutes} before you try stealing again." | None -> Ok attacker @@ -157,7 +154,15 @@ let handleSteal (ctx : IDiscordContext) = let num = rand.NextDouble() let result = winPercentage >= num , rand.Next(0,3) = 0 let dp = { DiscordPlayer.Id = targetId ; DiscordPlayer.Name = targetName } - let stealAction result = { ItemId = -1 ; Type = PlayerEventType.Steal ; Result = result ; Adversary = dp ; Timestamp = DateTime.UtcNow } + let stealAction result = { + ItemId = -1 + Type = PlayerEventType.Steal + Result = result + Adversary = dp + IsInstigator = true + Cooldown = ThiefCooldown.Minutes * 1 + Timestamp = DateTime.UtcNow + } let getResultEmbed' = getResultEmbed winPercentage prize thief.Bank thief dp // TODO: Send event to the hall of privacy // TODO: We need to check if the player is on cooldown @@ -169,12 +174,29 @@ let handleSteal (ctx : IDiscordContext) = do! Messaging.sendFollowUpEmbed ctx (embed.Build()) match! DbService.tryFindPlayer targetId with | Some t -> - let mugged = { ItemId = -1 ; Type = PlayerEventType.Steal ; Result = PlayerEventResult.Negative ; Adversary = thief.basicPlayer ; Timestamp = DateTime.UtcNow } - let actions = t |> Player.removeExpiredActions false |> fun p -> Array.append [| mugged |] p.Events + let mugged = { + ItemId = -1 + Type = PlayerEventType.Steal + Result = PlayerEventResult.Negative + Adversary = thief.basicPlayer + Timestamp = DateTime.UtcNow + IsInstigator = false + Cooldown = VictimRecovery.Minutes * 1 + } + let actions = t |> Player.removeExpiredActions |> fun p -> Array.append [| mugged |] p.Events do! DbService.updatePlayer { t with Bank = max (t.Bank - prize) 0 ; Events = actions } | None -> () - let stole = { ItemId = -1 ; Type = PlayerEventType.Steal ; Result = PlayerEventResult.Positive ; Adversary = dp ; Timestamp = DateTime.UtcNow } - let actions = thief |> Player.removeExpiredActions false |> fun p -> Array.append [| stole |] p.Events + + let stole = { + ItemId = -1 + Type = PlayerEventType.Steal + Result = PlayerEventResult.Positive + Adversary = dp + Timestamp = DateTime.UtcNow + IsInstigator = true + Cooldown = ThiefCooldown.Minutes * 1 + } + let actions = thief |> Player.removeExpiredActions |> fun p -> Array.append [| stole |] p.Events do! DbService.updatePlayer { thief with Bank = thief.Bank + prize ; XP = thief.XP + 0 ; Events = actions } // do! Async.Sleep 2000 // do! ctx.FollowUp (XP.getRewardsEmbed 1 player) |> Async.AwaitTask @@ -194,7 +216,7 @@ let handleSteal (ctx : IDiscordContext) = let targetId = uint64 split.[2] Game.executePlayerActionWithTargetId true targetId ctx (fun attacker defender -> async { do! attacker - |> Player.removeExpiredActions false + |> Player.removeExpiredActions |> checkVictimStealingCooldown defender >>= checkThiefCooldown |> handleResultWithResponse ctx (handleYes defender ) diff --git a/Bot/Trainer.fs b/Bot/Trainer.fs index a2a4cf7..423f997 100644 --- a/Bot/Trainer.fs +++ b/Bot/Trainer.fs @@ -13,16 +13,20 @@ let defaultHack = Armory.battleItems |> Array.find (fun i -> i.Id = int HackId.V let defaultShield = Armory.battleItems |> Array.find (fun i -> i.Id = int ShieldId.Firewall) let TrainerEvents = [| - { PlayerEvent.Timestamp = System.DateTime.UtcNow - PlayerEvent.Adversary = Sensei - PlayerEvent.Type = PlayerEventType.Hacking - PlayerEvent.Result = PlayerEventResult.Positive - PlayerEvent.ItemId = defaultHack.Id } - { PlayerEvent.Timestamp = System.DateTime.UtcNow - PlayerEvent.Adversary = DiscordPlayer.empty - PlayerEvent.Type = PlayerEventType.Shielding - PlayerEvent.Result = PlayerEventResult.Positive - PlayerEvent.ItemId = defaultShield.Id } + { Timestamp = System.DateTime.UtcNow + Adversary = Sensei + Type = PlayerEventType.Hacking + Result = PlayerEventResult.Positive + Cooldown = 5 + IsInstigator = true + ItemId = defaultHack.Id } + { Timestamp = System.DateTime.UtcNow + Adversary = DiscordPlayer.empty + Type = PlayerEventType.Shielding + Result = PlayerEventResult.Positive + Cooldown = defaultShield.Cooldown + IsInstigator = true + ItemId = defaultShield.Id } |] let sendInitialEmbed (client : DiscordClient) = @@ -156,19 +160,20 @@ let handleArsenal (ctx : IDiscordContext) = let hasStockWeapons = Player.getHacks player |> Array.exists (fun item -> item.Id = defaultHack.Id) let updatedPlayer = if not hasStockWeapons then { - Player.removeExpiredActions false player with + Player.removeExpiredActions player with Events = TrainerEvents |> Array.append player.Events Inventory = [| defaultHack ; defaultShield |] |> Array.append player.Inventory } else - Player.removeExpiredActions false player + Player.removeExpiredActions player if not hasStockWeapons then do! DbService.updatePlayer updatedPlayer let embed = Embeds.getArsenalEmbed updatedPlayer do! ctx.FollowUp(embed) |> Async.AwaitTask if not (player.Achievements |> Array.contains trainerAchievement) then do! Async.Sleep 3000 - let embed = Embeds.getAchievementEmbed "You completed the Training Dojo and collected loot." trainerAchievement + let rewards = [ $"{defaultHack.Name} Hack" ; $"{defaultShield.Name} Shield" ] + let embed = Embeds.getAchievementEmbed rewards "You completed the Training Dojo and collected loot." trainerAchievement do! ctx.FollowUp(embed) |> Async.AwaitTask do! Async.Sleep 2000 let role = ctx.GetGuild().GetRole(GuildEnvironment.roleTrainee) diff --git a/Shared/Shared.fs b/Shared/Shared.fs index 93ed5a3..fcfa385 100644 --- a/Shared/Shared.fs +++ b/Shared/Shared.fs @@ -59,7 +59,6 @@ module Types = } with static member empty = { Sell = false ; Buy = false ; Consume = false ; Drop = false } - type Item = { Id : int Name : string @@ -101,8 +100,10 @@ module Types = type PlayerEvent = { Type : PlayerEventType Result : PlayerEventResult + IsInstigator : bool Adversary : DiscordPlayer ItemId : int + Cooldown : int Timestamp : DateTime } []