Implement new hacker battle mechanics

This commit is contained in:
Joseph Ferano 2022-01-24 01:37:46 +07:00
parent 308409a1bc
commit ed68f0b87d
5 changed files with 201 additions and 130 deletions

View File

@ -40,7 +40,7 @@ let storeBot = new DiscordClient(storeConfig)
//let slotMachineBot = new DiscordClient(slotMachineConfig) //let slotMachineBot = new DiscordClient(slotMachineConfig)
//let clients = [| storeBot ; trainerBot ; hackerBattleBot ; playerInteractionsBot ; slotMachineBot |] //let clients = [| storeBot ; trainerBot ; hackerBattleBot ; playerInteractionsBot ; slotMachineBot |]
let clients = [| storeBot ; hackerBattleBot ; playerInteractionsBot |] let clients = [| storeBot ; hackerBattleBot ; playerInteractionsBot |]
let sc1 = playerInteractionsBot.UseSlashCommands() let sc1 = playerInteractionsBot.UseSlashCommands()
let sc3 = hackerBattleBot.UseSlashCommands() let sc3 = hackerBattleBot.UseSlashCommands()

View File

@ -7,7 +7,7 @@ open AsciiTableFormatter
let constructEmbed message = let constructEmbed message =
let builder = DiscordEmbedBuilder() let builder = DiscordEmbedBuilder()
builder.Color <- Optional(DiscordColor.PhthaloGreen) builder.Color <- Optional(DiscordColor.Blurple)
builder.Description <- message builder.Description <- message
let author = DiscordEmbedBuilder.EmbedAuthor() let author = DiscordEmbedBuilder.EmbedAuthor()
author.Name <- "Degenz Hacker Game" author.Name <- "Degenz Hacker Game"
@ -23,7 +23,7 @@ let pickDefense actionId player =
let embed = let embed =
DiscordEmbedBuilder() DiscordEmbedBuilder()
.WithColor(DiscordColor.PhthaloGreen) .WithColor(DiscordColor.Blurple)
.WithDescription("Pick a defense to mount for 10 hours") .WithDescription("Pick a defense to mount for 10 hours")
.WithImageUrl("https://s10.gifyu.com/images/Defense-Degenz-V2.gif") .WithImageUrl("https://s10.gifyu.com/images/Defense-Degenz-V2.gif")
@ -39,7 +39,7 @@ let pickHack actionId attacker defender =
let embed = let embed =
DiscordEmbedBuilder() DiscordEmbedBuilder()
.WithColor(DiscordColor.PhthaloGreen) .WithColor(DiscordColor.Blurple)
.WithDescription("Pick the hack that you want to use") .WithDescription("Pick the hack that you want to use")
.WithImageUrl("https://s10.gifyu.com/images/Hacker-Degenz-V2.gif") .WithImageUrl("https://s10.gifyu.com/images/Hacker-Degenz-V2.gif")
@ -49,6 +49,22 @@ let pickHack actionId attacker defender =
.AsEphemeral true .AsEphemeral true
let eventSuccessfulHack (event : ComponentInteractionCreateEventArgs) targetId prize = let eventSuccessfulHack (event : ComponentInteractionCreateEventArgs) targetId prize =
let embed =
DiscordEmbedBuilder()
.WithColor(DiscordColor.Blurple)
.WithDescription("Pick the hack that you want to use")
.WithImageUrl("https://s10.gifyu.com/images/Hacker-Degenz-V2.gif")
DiscordMessageBuilder()
.WithContent($"{event.User.Username} successfully hacked <@{targetId}> for a total of {prize} GoodBoyTokenz")
let eventFailedHack (event : ComponentInteractionCreateEventArgs) targetId prize =
let embed =
DiscordEmbedBuilder()
.WithColor(DiscordColor.Blurple)
.WithDescription("Pick the hack that you want to use")
.WithImageUrl("https://s10.gifyu.com/images/Hacker-Degenz-V2.gif")
DiscordMessageBuilder() DiscordMessageBuilder()
.WithContent($"{event.User.Username} successfully hacked <@{targetId}> for a total of {prize} GoodBoyTokenz") .WithContent($"{event.User.Username} successfully hacked <@{targetId}> for a total of {prize} GoodBoyTokenz")
@ -77,7 +93,7 @@ let storeListing store =
|> getClass |> getClass
{ Name = item.Name ; Cost = string item.Cost ; Class = string itemClass }) { Name = item.Name ; Cost = string item.Cost ; Class = string itemClass })
|> Formatter.Format |> Formatter.Format
|> sprintf "**%A**\n``` %s ```" itemType |> sprintf "**%As**\n``` %s ```" itemType
|> constructEmbed) |> constructEmbed)
DiscordInteractionResponseBuilder() DiscordInteractionResponseBuilder()

View File

@ -9,124 +9,141 @@ open DSharpPlus.SlashCommands
open Degenz open Degenz
open Degenz.Shared open Degenz.Shared
let checkForExistingHack attacker defenderId =
let updatedAttacks =
attacker.Attacks
|> removeExpiredActions (TimeSpan.FromHours(24)) (fun atk -> atk.Timestamp)
updatedAttacks
|> Array.tryFind (fun a -> a.Target.Id = defenderId)
|> function
| Some attack ->
let timeRemaining = TimeSpan.FromHours(24) - (DateTime.UtcNow - attack.Timestamp)
Error $"You can only hack the same target once every 24 hours, wait {timeRemaining.Seconds} seconds to attempt another hack on {attack.Target.Name}."
| None ->
Ok updatedAttacks
let checkIfHackHasCooldown hack updatedAttacks =
let mostRecentHackAttack =
updatedAttacks
|> Array.tryFind (fun a -> a.HackType = hack)
|> function
| Some a -> a.Timestamp
| None -> DateTime.UtcNow
if DateTime.UtcNow - mostRecentHackAttack <= TimeSpan.FromMinutes(5) then
Ok updatedAttacks
else
let timeRemaining = TimeSpan.FromMinutes(5) - (DateTime.UtcNow - mostRecentHackAttack)
Error $"You can only attack once a minute, wait {timeRemaining.Seconds} seconds to attack again."
let calculateDamage (hack: int) (shield: int) =
let hackClass = getClass hack
let protectionClass = getClass shield
match hackClass, protectionClass with
| h, p when h = p -> Weak
| _ -> Strong
let runHackerBattle attacker defender hack =
defender.Defenses
|> removeExpiredActions (TimeSpan.FromHours(6)) (fun (pro : Defense) -> pro.Timestamp)
|> Seq.toArray
|> Array.map (fun dfn -> int dfn.DefenseType)
|> Array.map (calculateDamage (int hack))
|> Array.contains Weak
let updateCombatants attacker defender hack prize =
let updatePlayer amount attack p =
{ p with Attacks = Array.append [| attack |] p.Attacks ; Bank = Math.Max(p.Bank + amount, 0) }
let attack = { HackType = enum<Hack>(int hack) ; Timestamp = DateTime.UtcNow ; Target = { Id = defender.DiscordId ; Name = defender.Name } }
[ DbService.updatePlayer <| updatePlayer prize attack attacker
DbService.updatePlayer <| modifyPlayerBank defender -prize ]
|> Async.Parallel
|> Async.Ignore
let successfulHack (event : ComponentInteractionCreateEventArgs) attacker defender hack =
async {
let prize = 3
do! updateCombatants attacker defender hack prize
let builder = DiscordInteractionResponseBuilder()
builder.IsEphemeral <- true
builder.Content <- $"Successfully hacked {defender.Name} using {hack}! You just won {prize} GoodBoyTokenz!"
// TODO: Don't make this an Update
do! event.Interaction.CreateResponseAsync(InteractionResponseType.UpdateMessage, builder)
|> Async.AwaitTask
let builder = Embeds.eventSuccessfulHack event defender.DiscordId prize
let channel = event.Guild.GetChannel(GuildEnvironment.channelEventsHackerBattle)
do! channel.SendMessageAsync(builder)
|> Async.AwaitTask
|> Async.Ignore
}
let failedHack (event : ComponentInteractionCreateEventArgs) attacker defender hack =
async {
let builder = DiscordInteractionResponseBuilder()
let prize = 2
builder.IsEphemeral <- true
builder.Content <- $"Hack failed! {defender.Name} was able to mount a successful defense! You lost {prize} GoodBoyTokenz!"
do! event.Interaction.CreateResponseAsync(InteractionResponseType.UpdateMessage, builder)
|> Async.AwaitTask
do! updateCombatants attacker defender hack -prize
let builder = DiscordMessageBuilder()
builder.WithContent($"Hacking attempt failed! <@{defender.DiscordId}> defended hack from {event.User.Username} and took {prize} from them! ") |> ignore
let channel = (event.Guild.GetChannel(GuildEnvironment.channelEventsHackerBattle))
do! channel.SendMessageAsync(builder)
|> Async.AwaitTask
|> Async.Ignore
}
let attack (ctx : InteractionContext) (target : DiscordUser) = let attack (ctx : InteractionContext) (target : DiscordUser) =
async { async {
let! attacker = DbService.tryFindPlayer ctx.Member.Id let! attacker = DbService.tryFindPlayer ctx.Member.Id
let! defender = DbService.tryFindPlayer target.Id let! defender = DbService.tryFindPlayer target.Id
match attacker , defender with match attacker , defender with
| Some attacker , Some defender -> | Some attacker , Some defender ->
let updatedAttacks = let existingHack = checkForExistingHack attacker defender.DiscordId
attacker.Attacks match existingHack with
|> removeExpiredActions (TimeSpan.FromMinutes(15)) (fun (atk : Attack) -> atk.Timestamp) | Ok _ ->
do! DbService.updatePlayer <| { attacker with Attacks = updatedAttacks } let embed = Embeds.pickHack "Attack" attacker defender
if updatedAttacks.Length < 2 then do! ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, embed)
let embed = Embeds.pickHack "Attack" attacker defender |> Async.AwaitTask
| Error msg ->
do! ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, embed) let builder = DiscordInteractionResponseBuilder().WithContent(msg).AsEphemeral(true)
|> Async.AwaitTask do! ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, builder)
|> Async.AwaitTask
else
let builder = DiscordInteractionResponseBuilder()
let timestamp = updatedAttacks |> Array.rev |> Array.head |> fun a -> a.Timestamp // This should be the next expiring timestamp
let timeRemaining = TimeSpan.FromMinutes(15) - (DateTime.UtcNow - timestamp)
builder.Content <- $"No more hacks available, please wait {timeRemaining.Minutes} minutes and {timeRemaining.Seconds} seconds to attempt another hack"
builder.AsEphemeral true |> ignore
do! ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, builder)
|> Async.AwaitTask
| None , _ -> do! notYetAHackerMsg ctx | None , _ -> do! notYetAHackerMsg ctx
| _ , None -> do! createSimpleResponseAsync "Your target is not connected to the network, they must join first by using the /redpill command" ctx | _ , None -> do! createSimpleResponseAsync "Your target is not connected to the network, they must join first by using the /redpill command" ctx
} |> Async.StartAsTask } |> Async.StartAsTask
:> Task :> Task
let defend (ctx : InteractionContext) =
async {
let! player = DbService.tryFindPlayer ctx.Member.Id
match player with
| Some player ->
let updatedDefenses = removeExpiredActions (TimeSpan.FromHours(24)) (fun (pro : Defense) -> pro.Timestamp) player.Defenses
do! DbService.updatePlayer <| { player with Defenses = updatedDefenses }
if updatedDefenses.Length < 3 then
let embed = Embeds.pickDefense "Defend" player
do! ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, embed)
|> Async.AwaitTask
else
let builder = DiscordInteractionResponseBuilder()
let timestamp = updatedDefenses |> Array.rev |> Array.head |> fun a -> a.Timestamp // This should be the next expiring timestamp
let timeRemaining = TimeSpan.FromMinutes(15) - (DateTime.UtcNow - timestamp)
// TODO: Make this handle hours and minutes
builder.Content <- $"Cannot add new defense, please wait {timeRemaining.Hours} hours and {timeRemaining.Minutes} minutes to add another defense"
builder.AsEphemeral true |> ignore
do! ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, builder)
|> Async.AwaitTask
| None -> do! notYetAHackerMsg ctx
} |> Async.StartAsTask
:> Task
let handleAttack (event : ComponentInteractionCreateEventArgs) = let handleAttack (event : ComponentInteractionCreateEventArgs) =
let updatePlayer amount attack p =
{ p with Attacks = Array.append [| attack |] p.Attacks ; Bank = Math.Max(p.Bank + amount, 0) }
async { async {
let split = event.Id.Split("-") let split = event.Id.Split("-")
let weapon = Enum.Parse(typedefof<Hack>, split.[1]) :?> Hack let hack = Enum.Parse(typedefof<Hack>, split.[1]) :?> Hack
let ( resultId , targetId ) = UInt64.TryParse split.[2] let ( resultId , targetId ) = UInt64.TryParse split.[2]
let! resultPlayer = DbService.tryFindPlayer event.User.Id let! resultPlayer = DbService.tryFindPlayer event.User.Id
let! resultTarget = DbService.tryFindPlayer targetId let! resultTarget = DbService.tryFindPlayer targetId
// TODO: Do not let player hack themselves
match resultPlayer , resultTarget , true , resultId with match resultPlayer , resultTarget , true , resultId with
| Some player , Some target , true , true -> | Some attacker , Some defender , true , true ->
let updatedDefenses = removeExpiredActions (TimeSpan.FromHours(24)) (fun (p : Defense) -> p.Timestamp) target.Defenses do! checkForExistingHack attacker defender.DiscordId
do! DbService.updatePlayer <| { player with Defenses = updatedDefenses } |> Result.bind (checkIfHackHasCooldown hack)
let wasSuccessfulHack = |> function
updatedDefenses | Ok _ ->
|> Seq.toArray runHackerBattle attacker defender hack
|> Array.map (fun dfn -> int dfn.DefenseType) |> function
|> Array.map (calculateDamage (int weapon)) | false -> successfulHack event attacker defender hack
|> Array.contains Weak | true -> failedHack event attacker defender hack
match wasSuccessfulHack with | Error msg ->
| false -> let builder = DiscordInteractionResponseBuilder()
// let prize = 1.337f // LEET event.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, builder.WithContent(msg))
let prize = 13 |> Async.AwaitTask
let attack = { HackType = enum<Hack>(int weapon) ; Timestamp = DateTime.UtcNow ; Target = { Id = targetId ; Name = split.[3] } }
let! _ =
[ DbService.updatePlayer <| updatePlayer prize attack player
DbService.updatePlayer { target with Bank = Math.Max(target.Bank - prize, 0)} ]
|> Async.Parallel
let builder = DiscordInteractionResponseBuilder()
builder.IsEphemeral <- true
builder.Content <- $"Successfully hacked {split.[3]} using {weapon}! You just won {prize} GoodBoyTokenz!"
do! event.Interaction.CreateResponseAsync(InteractionResponseType.UpdateMessage, builder)
|> Async.AwaitTask
let builder = Embeds.eventSuccessfulHack event targetId prize
let channel = event.Guild.GetChannel(GuildEnvironment.channelEventsHackerBattle)
do! channel.SendMessageAsync(builder)
|> Async.AwaitTask
|> Async.Ignore
| true ->
let builder = DiscordInteractionResponseBuilder()
let prize = 2
builder.IsEphemeral <- true
builder.Content <- $"Hack failed! {split.[3]} was able to mount a successful defense! You lost {prize} GoodBoyTokenz!"
do! event.Interaction.CreateResponseAsync(InteractionResponseType.UpdateMessage, builder)
|> Async.AwaitTask
let attack = { HackType = enum<Hack>(int weapon) ; Timestamp = DateTime.UtcNow ; Target = { Id = targetId ; Name = split.[3] } }
do! DbService.updatePlayer <| updatePlayer -prize attack player
do! DbService.updatePlayer { target with Bank = target.Bank + prize }
let builder = DiscordMessageBuilder()
builder.WithContent($"Hacking attempt failed! <@{targetId}> defended hack from {event.User.Username} and took {prize} from them! ") |> ignore
let channel = (event.Guild.GetChannel(GuildEnvironment.channelEventsHackerBattle))
do! channel.SendMessageAsync(builder)
|> Async.AwaitTask
|> Async.Ignore
| _ -> | _ ->
let builder = DiscordInteractionResponseBuilder() let builder = DiscordInteractionResponseBuilder()
builder.IsEphemeral <- true builder.IsEphemeral <- true
@ -135,6 +152,25 @@ let handleAttack (event : ComponentInteractionCreateEventArgs) =
|> Async.AwaitTask |> Async.AwaitTask
} }
let defend (ctx : InteractionContext) =
async {
let! player = DbService.tryFindPlayer ctx.Member.Id
match player with
| Some player ->
if player.Shields.Length > 0 then
let embed = Embeds.pickDefense "Defend" player
do! ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, embed)
|> Async.AwaitTask
else
let builder = DiscordInteractionResponseBuilder()
builder.Content <- $"You currently do not have any Shields to protect your system. Please go to the store and purchase one."
builder.AsEphemeral true |> ignore
do! ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, builder)
|> Async.AwaitTask
| None -> do! notYetAHackerMsg ctx
} |> Async.StartAsTask
:> Task
let handleDefense (event : ComponentInteractionCreateEventArgs) = let handleDefense (event : ComponentInteractionCreateEventArgs) =
async { async {
let split = event.Id.Split("-") let split = event.Id.Split("-")
@ -142,21 +178,46 @@ let handleDefense (event : ComponentInteractionCreateEventArgs) =
let! playerResult = DbService.tryFindPlayer event.User.Id let! playerResult = DbService.tryFindPlayer event.User.Id
match playerResult , shieldResult with match playerResult , shieldResult with
| Some player , true -> | Some player , true ->
let builder = DiscordInteractionResponseBuilder() // TODO: All of this is wrong
builder.IsEphemeral <- true let updatedDefenses = removeExpiredActions (TimeSpan.FromHours(6)) (fun (pro : Defense) -> pro.Timestamp) player.Defenses
builder.Content <- $"Mounted a {shield} defense for 24 hours" let alreadyUsedShield = updatedDefenses |> Array.exists (fun d -> d.DefenseType = shield)
do! event.Interaction.CreateResponseAsync(InteractionResponseType.UpdateMessage, builder)
|> Async.AwaitTask
let defense = { DefenseType = shield ; Timestamp = DateTime.UtcNow } match alreadyUsedShield , updatedDefenses.Length < 2 with
do! DbService.updatePlayer <| { player with Defenses = Array.append [| defense |] player.Defenses } | false , true ->
let builder = DiscordInteractionResponseBuilder()
builder.IsEphemeral <- true
builder.Content <- $"Mounted a {shield} defense for 6 hours"
do! event.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, builder)
|> Async.AwaitTask
let defense = { DefenseType = shield ; Timestamp = DateTime.UtcNow }
do! DbService.updatePlayer <| { player with Defenses = Array.append [| defense |] player.Defenses }
let builder = DiscordMessageBuilder()
builder.WithContent($"{event.User.Username} has protected their system!") |> ignore
let channel = event.Guild.GetChannel(GuildEnvironment.channelEventsHackerBattle)
do! channel.SendMessageAsync(builder)
|> Async.AwaitTask
|> Async.Ignore
| _ , false ->
let builder = DiscordInteractionResponseBuilder()
builder.IsEphemeral <- true
let timestamp = updatedDefenses |> Array.rev |> Array.head |> fun a -> a.Timestamp // This should be the next expiring timestamp
let timeRemaining = TimeSpan.FromMinutes(15) - (DateTime.UtcNow - timestamp)
let hours = if timeRemaining.Hours > 0 then $"{timeRemaining.Hours} hours and " else ""
builder.Content <- $"You are only allowed two shields at a time. Wait {hours}{timeRemaining.Minutes} minutes to add another shield"
do! event.Interaction.CreateResponseAsync(InteractionResponseType.UpdateMessage, builder)
|> Async.AwaitTask
do! DbService.updatePlayer <| { player with Defenses = updatedDefenses }
| true , _ ->
let builder = DiscordInteractionResponseBuilder()
builder.IsEphemeral <- true
let timestamp = updatedDefenses |> Array.find (fun d -> d.DefenseType = shield) |> fun a -> a.Timestamp
let timeRemaining = TimeSpan.FromMinutes(15) - (DateTime.UtcNow - timestamp)
let hours = if timeRemaining.Hours > 0 then $"{timeRemaining.Hours} hours and " else ""
builder.Content <- $"{shield} shield is already in use. Wait {hours}{timeRemaining.Minutes} minutes to use this shield again"
do! event.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, builder)
|> Async.AwaitTask
do! DbService.updatePlayer <| { player with Defenses = updatedDefenses }
let builder = DiscordMessageBuilder()
builder.WithContent($"{event.User.Username} has protected their system!") |> ignore
let channel = event.Guild.Channels.Values |> Seq.find (fun c -> c.Name = "battle-1")
do! channel.SendMessageAsync(builder)
|> Async.AwaitTask
|> Async.Ignore
| _ -> | _ ->
let builder = DiscordInteractionResponseBuilder() let builder = DiscordInteractionResponseBuilder()
builder.IsEphemeral <- true builder.IsEphemeral <- true

View File

@ -106,13 +106,13 @@ module Commands =
let! player = DbService.tryFindPlayer ctx.Member.Id let! player = DbService.tryFindPlayer ctx.Member.Id
match player with match player with
| Some p -> | Some p ->
// TODO: Is this working? let updatedAttacks = p.Attacks |> removeExpiredActions (TimeSpan.FromHours(24)) (fun (atk : Attack) -> atk.Timestamp)
let updatedAttacks = p.Attacks |> removeExpiredActions (TimeSpan.FromMinutes(15)) (fun (atk : Attack) -> atk.Timestamp) let updatedDefenses = p.Defenses |> removeExpiredActions (TimeSpan.FromHours(6)) (fun (p : Defense) -> p.Timestamp)
let updatedDefenses = p.Defenses |> removeExpiredActions (TimeSpan.FromHours(24)) (fun (p : Defense) -> p.Timestamp) let updatedPlayer = { p with Attacks = updatedAttacks ; Defenses = updatedDefenses }
do! DbService.updatePlayer <| { p with Attacks = updatedAttacks ; Defenses = updatedDefenses } do! DbService.updatePlayer updatedPlayer
let builder = DiscordInteractionResponseBuilder() let builder = DiscordInteractionResponseBuilder()
builder.IsEphemeral <- true builder.IsEphemeral <- true
builder.Content <- statusFormat p builder.Content <- statusFormat updatedPlayer
do! ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, builder) do! ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, builder)
|> Async.AwaitTask |> Async.AwaitTask
| None -> do! notYetAHackerMsg ctx | None -> do! notYetAHackerMsg ctx

View File

@ -106,13 +106,7 @@ let constructButtons (actionType: string) (playerInfo: string) (weapons: 'a arra
let removeExpiredActions timespan (timestamp: 'a -> DateTime) actions = let removeExpiredActions timespan (timestamp: 'a -> DateTime) actions =
actions |> Array.filter (fun act -> DateTime.UtcNow - (timestamp act) < timespan) actions |> Array.filter (fun act -> DateTime.UtcNow - (timestamp act) < timespan)
let calculateDamage (hack: int) (shield: int) = let modifyPlayerBank player amount = { player with Bank = Math.Max(player.Bank + amount, 0) }
let hackClass = getClass hack
let protectionClass = getClass shield
match hackClass, protectionClass with
| h, p when h = p -> Weak
| _ -> Strong
module Message = module Message =
let sendFollowUpMessage (event : ComponentInteractionCreateEventArgs) msg = let sendFollowUpMessage (event : ComponentInteractionCreateEventArgs) msg =