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