diff --git a/CustomRoles.cs b/CustomRoles.cs index c5c6ecdff..be4764212 100644 --- a/CustomRoles.cs +++ b/CustomRoles.cs @@ -510,6 +510,7 @@ public enum CustomRoles BananaMan, Beartrap, Bewilder, + Blessed, Blind, Blocked, Bloodlust, diff --git a/Main.cs b/Main.cs index ee4155f63..6d76f8fff 100644 --- a/Main.cs +++ b/Main.cs @@ -670,6 +670,7 @@ public class Main : BasePlugin { CustomRoles.Listener, "#060270" }, { CustomRoles.Unbound, "#DFC57B" }, { CustomRoles.AntiTP, "#fcba03" }, + { CustomRoles.Blessed, "#7bfbff" }, { CustomRoles.Hidden, "#E2EAF4" }, { CustomRoles.Looter, "#F5D866" }, { CustomRoles.Tired, "#ff1919" }, diff --git a/Modules/RPC.cs b/Modules/RPC.cs index 0d9dff919..2629580c2 100644 --- a/Modules/RPC.cs +++ b/Modules/RPC.cs @@ -164,6 +164,7 @@ public enum CustomRPC SetChatVisible, Exclusionary, Deadlined, + Blessed, // Game Modes RoomRushDataSync, @@ -1220,9 +1221,8 @@ internal static class RPCHandlerPatch { byte id = reader.ReadByte(); (Main.PlayerStates[id].Role as Werewolf)?.ReceiveRPC(reader); - } - break; + } case CustomRPC.SetSwapperVotes: { Swapper.ReceiveRPC(reader, __instance); @@ -1416,6 +1416,16 @@ internal static class RPCHandlerPatch break; } + case CustomRPC.Deadlined: + { + Deadlined.ReceiveRPC(reader); + break; + } + case CustomRPC.Blessed: + { + Blessed.ReceiveRPC(reader); + break; + } } } catch (Exception e) { Utils.ThrowException(e); } diff --git a/Modules/Utils.cs b/Modules/Utils.cs index b6e299f7e..10b8dd488 100644 --- a/Modules/Utils.cs +++ b/Modules/Utils.cs @@ -2795,6 +2795,7 @@ public static class Utils if (seer.Is(CustomRoles.Deadlined)) additionalSuffixes.Add(Deadlined.GetSuffix(seer)); if (seer.Is(CustomRoles.Introvert)) additionalSuffixes.Add(Introvert.GetSelfSuffix(seer)); if (seer.Is(CustomRoles.Allergic)) additionalSuffixes.Add(Allergic.GetSelfSuffix(seer)); + if (seer.Is(CustomRoles.Blessed)) additionalSuffixes.Add(Blessed.GetSuffix(seer)); additionalSuffixes.Add(Bloodmoon.GetSuffix(seer)); additionalSuffixes.Add(Haunter.GetSuffix(seer)); @@ -3854,6 +3855,7 @@ public static class Utils Stressed.AfterMeetingTasks(); Circumvent.AfterMeetingTasks(); Deadlined.AfterMeetingTasks(); + Blessed.AfterMeetingTasks(); Tired.Reset(); } catch (Exception e) { ThrowException(e); } diff --git a/Patches/HudPatch.cs b/Patches/HudPatch.cs index a6c56f4c5..15c4e61d1 100644 --- a/Patches/HudPatch.cs +++ b/Patches/HudPatch.cs @@ -292,6 +292,7 @@ internal static class HudManagerPatch CustomRoles.Dynamo => Dynamo.GetSuffix(player, true), CustomRoles.Deadlined => Deadlined.GetSuffix(player, true), CustomRoles.Introvert => Introvert.GetSelfSuffix(player), + CustomRoles.Blessed => Blessed.GetSuffix(player), _ => string.Empty }).Where(x => !string.IsNullOrWhiteSpace(x)).ToArray(); diff --git a/Patches/OnGameStartedPatch.cs b/Patches/OnGameStartedPatch.cs index 13b4386cc..383a43ea3 100644 --- a/Patches/OnGameStartedPatch.cs +++ b/Patches/OnGameStartedPatch.cs @@ -256,6 +256,7 @@ internal static class ChangeRoleSettings Lazy.BeforeMeetingPositions = []; Introvert.TeleportAwayDelays = []; Onbound.NumBlocked = []; + Blessed.ShieldActive = []; try { diff --git a/Patches/PlayerControlPatch.cs b/Patches/PlayerControlPatch.cs index 8f57f5ec1..e9c9dfce6 100644 --- a/Patches/PlayerControlPatch.cs +++ b/Patches/PlayerControlPatch.cs @@ -425,6 +425,7 @@ internal static class CheckMurderPatch Medic.OnAnyoneCheckMurder(killer, target) || Randomizer.IsShielded(target) || Aid.ShieldedPlayers.ContainsKey(target.PlayerId) || + Blessed.ShieldActive.Contains(target.PlayerId) || Gaslighter.IsShielded(target) || !Farmer.OnAnyoneCheckMurder(target) || !PotionMaster.OnAnyoneCheckMurder(target) || diff --git a/Resources/Lang/en_US.json b/Resources/Lang/en_US.json index dd09bfe26..26123e66d 100644 --- a/Resources/Lang/en_US.json +++ b/Resources/Lang/en_US.json @@ -9346,6 +9346,11 @@ "SafeguardInfoLong": "(Crewmates):\\nAs the Safeguard, you get a temporary shield with each task you complete.", "SafeguardSuffixTimer": "<#00ffa5>You're shielded for {0}s", "SafeguardSuffix": "<#00ffa5>You're shielded", + "Blessed": "Blessed", + "BlessedInfo": "Shielded Temporarily After Meetings", + "BlessedInfoLong": "(Add-ons):\\nAs the Blessed, you get a shield after every meeting for a defined number of seconds.", + "Blessed.MinLivingPlayersToActivateShield": "Minimum Living Players for the Shield to Activate", + "Prefix.Blessed": "Blessed", "Telekinetic": "Telekinetic", "TelekineticInfo": "Execute Different Actions to Help the Crew", "TelekineticInfoLong": "(Crewmates):\\nSees a timer at all times. Dies if it gets above 60s. They can vent to change modes. They can only have one mode activated at a time. To execute an action, pet. Frozen for 5s after using some powers, timer is 40s after a meeting. Each action increases the timer by a certain amount when used (indicated next to the action name in the following list in brackets)\\n\\nModes:\\nTeleport Target: teleports the nearest player outside the map (+40s)\\nTeleport Self: teleports themself outside the map (+30s)\\nFreeze: freezes the nearest player (+35s)\\nKill: instantly kills the nearest player (+60s)\\nShield: gives them a shield lasting for some time (+45s)\\nSpeed: gives them increased speed for some time (+30s)\\nDoors: opens all closed doors instantly (+35s)", diff --git a/Roles/Standard/Addons/Common/Blessed.cs b/Roles/Standard/Addons/Common/Blessed.cs new file mode 100644 index 000000000..afad31cda --- /dev/null +++ b/Roles/Standard/Addons/Common/Blessed.cs @@ -0,0 +1,80 @@ +using System.Collections.Generic; +using System.Diagnostics; +using EHR.Modules; +using EHR.Modules.Extensions; +using Hazel; + +namespace EHR.Roles; + +public class Blessed : IAddon +{ + public AddonTypes Type => AddonTypes.Helpful; + + private static OptionItem ShieldDuration; + private static OptionItem MinLivingPlayersToActivateShield; + + public static HashSet ShieldActive = []; + private static Stopwatch ShieldTimer; + + public void SetupCustomOption() + { + Options.SetupAdtRoleOptions(657900, CustomRoles.Blessed, canSetNum: true, teamSpawnOptions: true); + + ShieldDuration = new IntegerOptionItem(657910, "ShieldDuration", new(1, 120, 1), 30, TabGroup.Addons) + .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Blessed]) + .SetValueFormat(OptionFormat.Seconds); + + MinLivingPlayersToActivateShield = new IntegerOptionItem(657911, "Blessed.MinLivingPlayersToActivateShield", new(1, 15, 1), 6, TabGroup.Addons) + .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Blessed]) + .SetValueFormat(OptionFormat.Players); + } + + public static void AfterMeetingTasks() + { + PlayerControl[] aapc = Main.AllAlivePlayerControls; + if (aapc.Length < MinLivingPlayersToActivateShield.GetInt()) return; + + foreach (PlayerControl pc in aapc) + { + if (!pc.Is(CustomRoles.Blessed)) continue; + ShieldActive.Add(pc.PlayerId); + } + + ShieldTimer = Stopwatch.StartNew(); + Utils.SendRPC(CustomRPC.Blessed, 1); + Main.Instance.StartCoroutine(CoRoutine()); + return; + + System.Collections.IEnumerator CoRoutine() + { + int duration = ShieldDuration.GetInt(); + while (ShieldTimer.GetRemainingTime(duration) > 0 && !(GameStates.IsMeeting || ExileController.Instance || GameStates.IsEnded || !GameStates.InGame)) yield return null; + + ShieldTimer.Reset(); + Utils.SendRPC(CustomRPC.Blessed, 2); + + if (GameStates.IsInTask && !ExileController.Instance && !AntiBlackout.SkipTasks) + ShieldActive.ToValidPlayers().Do(x => Utils.NotifyRoles(SpecifySeer: x, SpecifyTarget: x)); + + ShieldActive = []; + } + } + + public static void ReceiveRPC(MessageReader reader) + { + switch (reader.ReadPackedInt32()) + { + case 1: + ShieldTimer = Stopwatch.StartNew(); + break; + case 2: + ShieldTimer.Reset(); + break; + } + } + + public static string GetSuffix(PlayerControl seer) + { + return !ShieldTimer.IsRunning ? string.Empty : $"{(seer.IsModdedClient() ? string.Format(Translator.GetString("SafeguardSuffixTimer"), ShieldTimer.GetRemainingTime(ShieldDuration.GetInt())) : Translator.GetString("SafeguardSuffix"))}"; + } +} \ No newline at end of file