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

356 lines
16 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 weapon 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 checkTargetCompletedTraining target (player : PlayerData) = async {
let! targetCompletedTraining = DbService.checkHasAchievement target.DiscordId Trainer.TrainerAchievement
if targetCompletedTraining || not target.Inventory.IsEmpty
then return Ok player
else return Error $"Looks like they haven't completed training with Sensei yet, you can't hack noobs..."
}
let runHackerBattle defender (hack : HackItem) =
defender
|> Player.removeExpiredActions
|> fun p -> p.Events
|> List.exists (fun event ->
match event.Type with
| Shielding id -> hack.Class = (Inventory.findShieldById id Armory.weapons).Class
| _ -> false)
let updateCombatants successfulHack (attacker : PlayerData) (defender : PlayerData) (hack : HackItem) prize =
let updatePlayer amount attack p =
{ p with Events = attack::p.Events ; Bank = max (p.Bank + amount) 0<GBT> }
let event isDefenderEvent =
let hackEvent = {
HackId = hack.Item.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.updatePlayer <| updatePlayer prize (event false) attacker
DbService.updatePlayer <| updatePlayer -prize (event true) defender
DbService.addPlayerEvent attacker.DiscordId (event false)
DbService.addPlayerEvent defender.DiscordId (event true) ]
|> Async.Parallel
|> Async.Ignore
let successfulHack (ctx : IDiscordContext) attacker defender (hack : HackItem) =
async {
let prizeAmount = if hack.Power < int defender.Bank then hack.Power else int defender.Bank
do! updateCombatants true attacker defender hack (prizeAmount * 1<GBT>)
let embed = Embeds.responseSuccessfulHack true defender.DiscordId prizeAmount hack
do! ctx.FollowUp embed |> Async.AwaitTask
let builder = Embeds.eventSuccessfulHack ctx defender prizeAmount
let channel = ctx.GetGuild().GetChannel(GuildEnvironment.channelEventsHackerBattle)
do! channel.SendMessageAsync(builder)
|> Async.AwaitTask
|> Async.Ignore
}
let failedHack (ctx : IDiscordContext) attacker defender (hack : HackItem) =
async {
let lostAmount = if hack.Power < int attacker.Bank then hack.Power else int attacker.Bank
let msg = $"Hack failed! {defender.Name} was able to mount a successful defense! You lost {lostAmount} $GBT!"
do! sendFollowUpMessage ctx msg
do! updateCombatants false attacker defender hack (-lostAmount * 1<GBT>)
let builder = DiscordMessageBuilder()
builder.WithContent($"Hacking attempt failed! <@{defender.DiscordId}> defended hack from {ctx.GetDiscordMember().Username} and took {lostAmount} $GBT from them! ") |> ignore
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 {
let! result = checkTargetCompletedTraining defender attacker
do! attacker
|> Player.removeExpiredActions
|> checkAlreadyHackedTarget defender
>>= fun p -> match result with Ok _ -> Ok p | Error e -> Error e
>>= checkTargetHasFunds defender
>>= checkHasEmptyHacks
>>= checkPlayerIsAttackingThemselves defender
|> function
| Ok atkr -> async {
let embed = Embeds.pickHack "Attack" atkr defender 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) =
executePlayerAction ctx (fun attacker -> async {
let tokens = ctx.GetInteractionId().Split("-")
let hackId = int tokens.[1]
let hack = Armory.weapons |> Inventory.findHackById hackId
let resultId , targetId = UInt64.TryParse tokens.[2]
let! resultTarget = DbService.tryFindPlayer targetId
match resultTarget , true , resultId with
| Some defender , true , true ->
do! attacker
|> Player.removeExpiredActions
|> checkAlreadyHackedTarget defender
>>= checkPlayerOwnsWeapon hack.Item
>>= checkTargetHasFunds defender
>>= checkWeaponHasCooldown hack.Item
|> function
| Ok atkr -> async {
let result = runHackerBattle defender hack
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)
}
| 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 = int tokens.[1]
let shield = Armory.weapons |> Inventory.findShieldById shieldId
do! player
|> checkPlayerOwnsWeapon shield.Item
>>= checkPlayerHasShieldSlotsAvailable
>>= checkWeaponHasCooldown shield.Item
|> handleResultWithResponse ctx (fun p -> async {
let embed = Embeds.responseCreatedShield shield
do! ctx.FollowUp embed |> Async.AwaitTask
let defense = {
Type = Shielding shieldId
Cooldown = shield.Cooldown
Timestamp = DateTime.UtcNow
}
do! DbService.updatePlayer p |> Async.Ignore
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()) shield.Item.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! DbService.updatePlayer updatedPlayer |> Async.Ignore
do! Analytics.arsenalCommand (ctx.GetDiscordMember())
})
let handleButtonEvent (_ : DiscordClient) (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 |> Async.StartAsTask :> Task
| 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 (_ : DiscordClient) (event : MessageCreateEventArgs) =
task {
if event.Channel.Id = GuildEnvironment.channelTraining && event.Author.Id <> GuildEnvironment.botIdHackerBattle then
do! Async.Sleep 1000
do! event.Message.DeleteAsync()
} :> 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