discord-bot-game/Bot/Games/HackerBattle.fs

380 lines
17 KiB
Forth

module Degenz.HackerBattle
open System
open System.Text
open System.Threading.Tasks
open DSharpPlus
open DSharpPlus.Entities
open DSharpPlus.EventArgs
open DSharpPlus.SlashCommands
open Degenz
open Degenz.Messaging
open Degenz.PlayerInteractions
let checkPlayerIsAttackingThemselves defender attacker =
match attacker.DiscordId = defender.DiscordId with
| true -> Error "You think you're clever? You can't hack yourself, pal."
| false -> Ok attacker
let checkAlreadyHackedTarget defender attacker =
defender
|> Player.removeExpiredActions
|> fun d -> d.Events
|> List.tryFind (fun event ->
match event.Type with
| Hacking h -> h.Adversary.Id = attacker.DiscordId && h.IsInstigator = false
| _ -> false)
|> function
| Some event ->
let cooldown = TimeSpan.FromMinutes(int event.Cooldown)
let cooldownText = getTimeText true cooldown event.Timestamp
Error $"You can only hack the same target once every {cooldown.Hours} hours, wait {cooldownText} to attempt another hack on <@{defender.DiscordId}>."
| None -> Ok attacker
let checkWeaponHasCooldown (weapon : Item) attacker =
attacker.Events
|> List.tryFind (fun a ->
match a.Type with
| Hacking h -> h.HackId = weapon.Id && h.IsInstigator
| Shielding id -> id = weapon.Id
| _ -> false)
|> function
| Some event ->
let cooldown = getTimeText true (TimeSpan.FromMinutes(int event.Cooldown)) event.Timestamp
Error $"{weapon.Name} was recently used, wait another {cooldown} to use it again."
| None -> Ok attacker
let checkHasEmptyHacks (attacker : PlayerData) =
match Inventory.getHacks attacker.Inventory with
| [] -> Error $"You currently do not have any Hacks to take 💰$GBT from others. Please go to the <#{GuildEnvironment.channelTraining}> and complete the training."
| _ -> Ok attacker
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 {item.Name} already, you cheeky bastard..."
let checkPlayerHasShieldSlotsAvailable player =
let updatedPlayer = player |> Player.removeExpiredActions
let defenses = Player.getShieldEvents updatedPlayer
match defenses |> List.length >= 3 with
| true ->
let event = defenses |> List.rev |> List.head // This should be the next expiring timestamp
let cooldown = getTimeText true (TimeSpan.FromMinutes(int event.Cooldown)) event.Timestamp
Error $"You are only allowed three shields at a time. Wait {cooldown} to add another shield"
| false -> Ok updatedPlayer
let checkTargetHasFunds target player =
match target.Bank <= 0<GBT> with
| true -> Error $"Looks like the poor bastard has no 💰$GBT... pick a different victim."
| false -> Ok player
let strengthBonus attacker defender =
attacker - defender
|> max 0
|> float
|> (*) 0.01
|> (*) 200.0 // Bonus
|> int
let runHackerBattle defender (hack : HackItem) =
defender
|> Player.removeExpiredActions
|> fun p -> p.Events
|> List.exists (fun event ->
match event.Type with
| Shielding id ->
let item = Inventory.findItemById id Arsenal.weapons
match item.Attributes with
| CanClass c -> hack.Class = c
| _ -> false
| _ -> false)
let updateCombatants successfulHack (attacker : PlayerData) (defender : PlayerData) (hack : HackItem) prize =
let event isDefenderEvent =
let hackEvent = {
HackId = hack.Id
Adversary = if isDefenderEvent then attacker.toDiscordPlayer else defender.toDiscordPlayer
IsInstigator = not isDefenderEvent
Success = successfulHack
}
{ Type = Hacking hackEvent
Timestamp = DateTime.UtcNow
Cooldown = if isDefenderEvent then int WeaponClass.SameTargetAttackCooldown.TotalMinutes * 1<mins> else hack.Cooldown }
[ DbService.updatePlayerCurrency (if successfulHack then prize else -prize) attacker.DiscordId
DbService.updatePlayerCurrency (if successfulHack then -prize else prize) defender.DiscordId
DbService.addPlayerEvent attacker.DiscordId (event false)
DbService.addPlayerEvent defender.DiscordId (event true) ]
|> Async.Parallel
|> Async.Ignore
let hackerResult successfulHack (ctx : IDiscordContext) attacker defender (hack : HackItem) =
async {
let prizeAmount , bonus =
if successfulHack then
let bonus = strengthBonus attacker.Stats.Strength.Amount defender.Stats.Strength.Amount |> gbt
let basePrize = gbt hack.Power
(if basePrize + bonus < defender.Bank then basePrize + bonus else defender.Bank) , bonus
else
if hack.Power < int attacker.Bank
then gbt hack.Power , 0<GBT>
else attacker.Bank , 0<GBT>
do! updateCombatants successfulHack attacker defender hack prizeAmount
let! defenderMember = ctx.GetGuild().GetMemberAsync(defender.DiscordId) |> Async.AwaitTask
let embed = Embeds.responseSuccessfulHack2 successfulHack attacker defender (ctx.GetDiscordMember()) defenderMember prizeAmount bonus hack
do! ctx.GetChannel().SendMessageAsync(embed)
|> Async.AwaitTask
|> Async.Ignore
let builder = DiscordMessageBuilder().WithAllowedMention(UserMention(defender.DiscordId))
if successfulHack then
builder.Content <- $"**{ctx.GetDiscordMember().Username}** successfully hacked **{defender.Name}** and took {prizeAmount} $GBT! - <@!{defender.DiscordId}>"
else
builder.Content <- $"Hacking attempt failed! **{defender.Name}** defended hack from **{ctx.GetDiscordMember().Username}** and took {prizeAmount} 💰$GBT from them! <@!{defender.DiscordId}>"
let channel = ctx.GetGuild().GetChannel(GuildEnvironment.channelEventsHackerBattle)
do! channel.SendMessageAsync(builder)
|> Async.AwaitTask
|> Async.Ignore
}
let hack (target : DiscordUser) (ctx : IDiscordContext) =
executePlayerActionWithTarget target ctx (fun attacker defender -> async {
do! attacker
|> Player.removeExpiredActions
|> checkAlreadyHackedTarget defender
>>= checkTargetHasFunds defender
>>= checkHasEmptyHacks
>>= checkPlayerIsAttackingThemselves defender
|> function
| Ok atkr -> async {
let bonus = strengthBonus attacker.Stats.Strength.Amount defender.Stats.Strength.Amount
let embed = Embeds.pickHack "Attack" atkr defender bonus false
do! ctx.FollowUp(embed) |> Async.AwaitTask
// TODO: Add a call when it's an error with the error type, must include an error code
do! Analytics.hackCommand (ctx.GetDiscordMember())
}
| Error msg -> sendFollowUpMessage ctx msg
})
let handleAttack (ctx : IDiscordContext) =
executePlayerActionNoMsg ctx (fun attacker -> async {
let tokens = ctx.GetInteractionId().Split("-")
let hackId = tokens.[1]
let item = Arsenal.weapons |> Inventory.findItemById hackId
let hackItem = (Inventory.getHackItem item).Value
let resultId , targetId = UInt64.TryParse tokens.[2]
let! resultTarget = DbService.tryFindPlayer targetId
match resultTarget , resultId with
| Some defender , true ->
do! attacker
|> Player.removeExpiredActions
|> checkAlreadyHackedTarget defender
>>= checkPlayerOwnsWeapon item
>>= checkTargetHasFunds defender
>>= checkWeaponHasCooldown item
|> function
| Ok attacker -> async {
let result = runHackerBattle defender hackItem
do! hackerResult (not result) 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"
})
let defend (ctx : IDiscordContext) =
executePlayerAction ctx (fun player -> async {
if player.Inventory |> Inventory.getShields |> List.length > 0 then
let p = Player.removeExpiredActions player
let embed = Embeds.pickDefense "Defend" p false
do! ctx.FollowUp embed |> Async.AwaitTask
do! Analytics.shieldCommand (ctx.GetDiscordMember())
else
let msg = $"You currently do not have any Shields to protect yourself from hacks. Please go to the <#{GuildEnvironment.channelTraining}> and purchase one."
do! Messaging.sendFollowUpMessage ctx msg
})
let handleDefense (ctx : IDiscordContext) =
executePlayerAction ctx (fun player -> async {
let tokens = ctx.GetInteractionId().Split("-")
let shieldId = tokens.[1]
let item = Arsenal.weapons |> Inventory.findItemById shieldId
let shieldItem = (Inventory.getShieldItem item).Value
do! player
|> checkPlayerOwnsWeapon item
>>= checkPlayerHasShieldSlotsAvailable
>>= checkWeaponHasCooldown item
|> handleResultWithResponse ctx (fun p -> async {
let embed = Embeds.responseCreatedShield shieldItem
do! ctx.FollowUp embed |> Async.AwaitTask
let defense = {
Type = Shielding shieldId
Cooldown = shieldItem.Cooldown
Timestamp = DateTime.UtcNow
}
do! DbService.addPlayerEvent p.DiscordId defense |> Async.Ignore
let builder = DiscordMessageBuilder()
builder.WithContent($"**{ctx.GetDiscordMember().Username}** has protected their system!") |> ignore
let channel = ctx.GetGuild().GetChannel(GuildEnvironment.channelEventsHackerBattle)
do! channel.SendMessageAsync(builder)
|> Async.AwaitTask
|> Async.Ignore
do! Analytics.shieldActivated (ctx.GetDiscordMember()) shieldItem.Name
})
})
let scan (ctx : IDiscordContext) =
executePlayerAction ctx (fun player -> async {
let! targets = DbService.getRandomHackablePlayers (ctx.GetDiscordMember().Id)
let targets =
let hackedIds =
player
|> Player.removeExpiredActions
|> fun p -> p.Events
|> List.choose (fun e -> match e.Type with | Hacking hack -> Some hack.Adversary.Id | _ -> None)
targets
|> List.filter (fun t -> hackedIds |> List.exists (fun hid -> hid = t.Id) |> not)
|> List.truncate 5
let sb = StringBuilder()
let mutable count = 0
for t in targets do
count <- count + 1
let result , user = ctx.GetGuild().Members.TryGetValue(t.Id)
if result then
sb.AppendLine($"{count}.) **{user.Username}**") |> ignore
else
printfn $"Could not find user {t.Id}"
sb.AppendLine($"{count}.) **{t.Name}**") |> ignore
let msg =
if targets.Length > 0 then
$"**Targets Connected to the Network:**\n\n These are 5 targets you can attempt to hack right now... let's hope they're not shielded!\n\n{sb}"
else
$"There don't seem to be any targets available right now"
let embed =
DiscordEmbedBuilder()
.WithColor(DiscordColor.Green)
.WithDescription(msg)
let builder =
DiscordFollowupMessageBuilder()
.AddEmbed(embed)
.AsEphemeral(true)
do! ctx.FollowUp(builder) |> Async.AwaitTask
})
let arsenal (ctx : IDiscordContext) =
executePlayerAction ctx (fun player -> async {
let updatedPlayer = Player.removeExpiredActions player
let builder = DiscordFollowupMessageBuilder()
let embed = DiscordEmbedBuilder()
embed.AddField("Arsenal", Arsenal.statusFormat updatedPlayer) |> ignore
builder.AddEmbed(embed) |> ignore
builder.IsEphemeral <- true
do! ctx.FollowUp(builder) |> Async.AwaitTask
do! Analytics.arsenalCommand (ctx.GetDiscordMember())
})
let handleButtonEvent _ (event : ComponentInteractionCreateEventArgs) =
let eventCtx = DiscordEventContext event :> IDiscordContext
match event.Id with
| id when id.StartsWith("Attack") -> handleAttack eventCtx
| id when id.StartsWith("Defend") -> handleDefense eventCtx
| id when id.StartsWith("Trainer") -> Trainer.handleButtonEvent eventCtx
| id when id.StartsWith("Steal") -> Thief.handleSteal eventCtx
| id when id.StartsWith("RPS") -> RockPaperScissors.handleRPS eventCtx
| _ ->
task {
let builder = DiscordInteractionResponseBuilder()
builder.IsEphemeral <- true
builder.Content <- $"Incorrect Action identifier {eventCtx.GetInteractionId()}"
do! eventCtx.Respond(InteractionResponseType.ChannelMessageWithSource, builder) |> Async.AwaitTask
}
let handleMessageCreated _ (event : MessageCreateEventArgs) =
task {
if event.Channel.Id = GuildEnvironment.channelTraining && event.Author.Id <> GuildEnvironment.botIdHackerBattle then
do! Async.Sleep 1000
do! event.Message.DeleteAsync()
} :> Task
let BeginnerProtectionHours = 24
let ShieldEvents () = [
{ Timestamp = System.DateTime.UtcNow
Cooldown = BeginnerProtectionHours * 60<mins>
Type = Shielding (string ItemId.FIREWALL) }
{ Timestamp = System.DateTime.UtcNow
Cooldown = BeginnerProtectionHours * 60<mins>
Type = Shielding (string ItemId.ENCRYPTION) }
{ Timestamp = System.DateTime.UtcNow
Cooldown = BeginnerProtectionHours * 60<mins>
Type = Shielding (string ItemId.CYPHER) }
]
let handleMemberUpdated _ (event : GuildMemberUpdateEventArgs) =
let addedRole (rolesBefore : DiscordRole seq) (rolesAfter : DiscordRole seq) =
rolesAfter |> Seq.filter ((fun role -> rolesBefore |> Seq.exists (fun r -> role.Id = r.Id)) >> not)
task {
let symmetricDifference = addedRole event.RolesBefore event.RolesAfter |> Seq.toList
match symmetricDifference with
| [] -> ()
| role::_ ->
if role.Name = "Pregen" then
do! ShieldEvents ()
|> List.map (DbService.addPlayerEvent event.Member.Id)
|> Async.Parallel
|> Async.Ignore
return ()
} :> Task
type HackerGame() =
inherit ApplicationCommandModule ()
let enforceChannels (ctx : IDiscordContext) (trainerFn : IDiscordContext -> Task) (battleFn : IDiscordContext -> Task) =
match ctx.GetChannel().Id with
| id when id = GuildEnvironment.channelTraining ->
let hasTraineeRole = Seq.exists (fun (r : DiscordRole) -> r.Id = GuildEnvironment.roleTrainee) (ctx.GetDiscordMember().Roles)
if hasTraineeRole then
trainerFn ctx
else
task {
let msg = $"You're currently not in training, either go to <#{GuildEnvironment.channelBattle}> to hack for real, "
+ "or restart training by clicking on the Pinned embed's button up top."
do! Messaging.sendSimpleResponse ctx msg
}
| id when id = GuildEnvironment.channelBattle ->
battleFn ctx
| _ ->
task {
let msg = $"You must go to <#{GuildEnvironment.channelBattle}> channel to hack, shield, or check your arsenal"
do! Messaging.sendSimpleResponse ctx msg
}
[<SlashCommand("arsenal", "Get the Hacks and Shields you own, and which ones are active")>]
member this.Arsenal (ctx : InteractionContext) =
enforceChannels (DiscordInteractionContext ctx) (Trainer.handleArsenal) arsenal
[<SlashCommand("hack", "Send a hack attack to another player")>]
member this.AttackCommand (ctx : InteractionContext, [<Option("target", "The player you want to hack")>] target : DiscordUser) =
enforceChannels (DiscordInteractionContext ctx) (Trainer.hack target) (hack target)
[<SlashCommand("shield", "Create a passive shield that will protect you for a certain time")>]
member this.ShieldCommand (ctx : InteractionContext) =
enforceChannels (DiscordInteractionContext ctx) Trainer.defend defend
[<SlashCommand("scan", "Find 5 targets connected to the network we can try to hack")>]
member this.ScanCommand (ctx : InteractionContext) =
enforceChannels (DiscordInteractionContext ctx) scan scan
// [<SlashCommand("test-autocomplete", "Create a passive defense that will last 24 hours")>]
member this.TestAutoComplete (ctx : InteractionContext) =
async {
let builder = DiscordInteractionResponseBuilder()
builder.IsEphemeral <- true
do! ctx.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, builder)
|> Async.AwaitTask
} |> Async.StartAsTask
:> Task