diff --git a/CTA/CustomTeamAssigner/CustomRoles.cs b/CTA/CustomTeamAssigner/CustomRoles.cs
index c0de42c18..183014ddf 100644
--- a/CTA/CustomTeamAssigner/CustomRoles.cs
+++ b/CTA/CustomTeamAssigner/CustomRoles.cs
@@ -273,6 +273,7 @@ public enum CustomRoles
SpeedBooster,
Spy,
SuperStar,
+ Survivor,
Swapper,
Tar,
TaskManager,
diff --git a/CustomRoles.cs b/CustomRoles.cs
index c0de42c18..183014ddf 100644
--- a/CustomRoles.cs
+++ b/CustomRoles.cs
@@ -273,6 +273,7 @@ public enum CustomRoles
SpeedBooster,
Spy,
SuperStar,
+ Survivor,
Swapper,
Tar,
TaskManager,
diff --git a/Main.cs b/Main.cs
index 2aebfec68..f05b80785 100644
--- a/Main.cs
+++ b/Main.cs
@@ -472,6 +472,7 @@ public class Main : BasePlugin
{ CustomRoles.Sheriff, "#ffb347" },
{ CustomRoles.CopyCat, "#ffb2ab" },
{ CustomRoles.SuperStar, "#f6f657" },
+ { CustomRoles.Survivor, "#989d12" },
{ CustomRoles.Ventguard, "#ffa5ff" },
{ CustomRoles.Demolitionist, "#5e2801" },
{ CustomRoles.Express, "#00ffff" },
diff --git a/Modules/CustomRolesHelper.cs b/Modules/CustomRolesHelper.cs
index 2138c0aaf..51e52a9f1 100644
--- a/Modules/CustomRolesHelper.cs
+++ b/Modules/CustomRolesHelper.cs
@@ -214,6 +214,7 @@ internal static class CustomRolesHelper
CustomRoles.LazyGuy => CustomRoles.Crewmate,
CustomRoles.Twister => UsePets ? CustomRoles.Impostor : CustomRoles.Shapeshifter,
CustomRoles.SuperStar => CustomRoles.Crewmate,
+ CustomRoles.Survivor => CustomRoles.Engineer,
CustomRoles.Anonymous => CustomRoles.Shapeshifter,
CustomRoles.Visionary => CustomRoles.Shapeshifter,
CustomRoles.Ninja => CustomRoles.Phantom,
@@ -1022,6 +1023,7 @@ internal static class CustomRolesHelper
CustomRoles.Mayor or
CustomRoles.Insight or
CustomRoles.Decryptor or
+ CustomRoles.Survivor or
CustomRoles.Transporter;
}
@@ -1479,6 +1481,7 @@ internal static class CustomRolesHelper
CustomRoles.Oxyman => RoleOptionType.Crewmate_Power,
CustomRoles.President => RoleOptionType.Crewmate_Power,
CustomRoles.Speedrunner => RoleOptionType.Crewmate_Power,
+ CustomRoles.Survivor => RoleOptionType.Crewmate_Power,
CustomRoles.Telekinetic => RoleOptionType.Crewmate_Power,
CustomRoles.Ankylosaurus => RoleOptionType.Crewmate_Chaos,
CustomRoles.Car => RoleOptionType.Crewmate_Chaos,
@@ -1953,10 +1956,10 @@ internal static class CustomRolesHelper
CustomRoles.Busy when !pc.GetTaskState().HasTasks => false,
CustomRoles.Truant when pc.Is(CustomRoles.SoulHunter) => false,
CustomRoles.Nimble when !pc.IsCrewmate() => false,
- CustomRoles.Physicist when !pc.IsCrewmate() || pc.GetCustomRole().IsDesyncRole() || (pc.Is(CustomRoles.TimeMaster) && !TimeMaster.TimeMasterCanUseVitals.GetBool()) => false,
- CustomRoles.Finder when !pc.IsCrewmate() || pc.GetCustomRole().IsDesyncRole() => false,
- CustomRoles.Noisy when !pc.IsCrewmate() || pc.GetCustomRole().IsDesyncRole() => false,
- CustomRoles.Examiner when !pc.IsCrewmate() || pc.GetCustomRole().IsDesyncRole() => false,
+ CustomRoles.Physicist when !pc.IsCrewmate() || pc.GetCustomRole().IsDesyncRole() || pc.Is(CustomRoles.Survivor) || (pc.Is(CustomRoles.TimeMaster) && !TimeMaster.TimeMasterCanUseVitals.GetBool()) => false,
+ CustomRoles.Finder when !pc.IsCrewmate() || pc.GetCustomRole().IsDesyncRole() || pc.Is(CustomRoles.Survivor) => false,
+ CustomRoles.Noisy when !pc.IsCrewmate() || pc.GetCustomRole().IsDesyncRole() || pc.Is(CustomRoles.Survivor) => false,
+ CustomRoles.Examiner when !pc.IsCrewmate() || pc.GetCustomRole().IsDesyncRole() || pc.Is(CustomRoles.Survivor) => false,
CustomRoles.Venom when !pc.IsImpostor() => false,
CustomRoles.Disregarded when pc.Is(CustomRoles.Bait) => false,
CustomRoles.Oblivious when pc.Is(CustomRoles.Coroner) => false,
diff --git a/Patches/HudSpritePatch.cs b/Patches/HudSpritePatch.cs
index 26ff61d5e..463606d54 100644
--- a/Patches/HudSpritePatch.cs
+++ b/Patches/HudSpritePatch.cs
@@ -572,6 +572,11 @@ public static class HudSpritePatch
newKillButton = CustomButton.Get("Shield");
break;
}
+ case CustomRoles.Survivor:
+ {
+ newAbilityButton = CustomButton.Get("Shield");
+ break;
+ }
case CustomRoles.Gangster when Gangster.CanRecruit(player.PlayerId):
case CustomRoles.Jackal when player.GetAbilityUseLimit() > 0:
{
diff --git a/Patches/MeetingHudPatch.cs b/Patches/MeetingHudPatch.cs
index c07f759cf..6bdfc719a 100644
--- a/Patches/MeetingHudPatch.cs
+++ b/Patches/MeetingHudPatch.cs
@@ -678,6 +678,9 @@ internal static class ExtendedMeetingHud
case Mayor mayor:
voteNum += Mayor.MayorAdditionalVote.GetInt() + mayor.TaskVotes;
break;
+ case Survivor survivor when Main.AllAlivePlayerControlsCount <= Survivor.ThirdAbility.GetInt():
+ voteNum += Survivor.AdditionalVote.GetInt();
+ break;
}
if (ps.TargetPlayerId == ps.VotedFor && Options.MadmateSpawnMode.GetInt() == 2 && CustomRoles.Madmate.IsEnable() && MeetingStates.FirstMeeting) voteNum = 0;
diff --git a/Resources/Lang/en_US.jsonc b/Resources/Lang/en_US.jsonc
index cdedd3522..ef42abfc1 100644
--- a/Resources/Lang/en_US.jsonc
+++ b/Resources/Lang/en_US.jsonc
@@ -1409,7 +1409,7 @@
"Spiritualist": "Spiritualist",
"SpiritualistInfo": "You see where a ghost is",
"SpiritualistInfoLong": "(Crewmates):\\nAs the Spiritualist, you will get an arrow pointing to the ghost of the body that was reported in the last meeting.\\n\\nIf the player was good, it may lead you to an Impostor or neutral killer.\\nIf the player was bad, it may just lead you to a fellow crewmate.",
- "SpiritualistNoticeMessage": "The Spiritualist has an arrow pointing to you!\\nYou can use them to a killer or frame a crewmate",
+ "SpiritualistNoticeMessage": "The Spiritualist has an arrow pointing to you!\\nYou can use them to point out a killer or frame a crewmate",
"SpiritualistShowGhostArrowEverySeconds": "Ghost arrow interval",
"SpiritualistShowGhostArrowForSeconds": "Ghost arrow duration",
"SpiritualistTargetMessage": "The Spiritualist has an arrow pointing toward you",
@@ -1421,6 +1421,15 @@
"SuperStarInfo": "Everyone knows you",
"SuperStarInfoLong": "(Crewmates):\\nThe Super Star has a star logo next to their name and can only be killed when the Murderer is alone with them. Guessers cannot guess them.",
"SuperStarDead": "Shock! Super Star {0} has unfortunately been mercilessly killed in the recent period of time!",
+ "Survivor": "Survivor",
+ "SurvivorInfo": "With each fallen crew, I get stronger",
+ "SurvivorInfoLong": "(Crewmates):\\nThe Survivor will get abilities with each set amount of players left in the game.\\n\\n- 1st Ability: See the number of alive players left in real time.\\n- 2nd Ability: Vent/Vanish to give yourself a temporary shield.\\n- 3rd Ability: An additional set amount of votes.\\n- Last Ability: Finish tasks to gain a Kill button.\\n\\nNote: All abilities are stackable, separated, and can shuffle their order if wanted, as well as disable them by set the required amount of players needed to 0 or 1.",
+ "SurvivorFirstAbility": "Number of players left to get 1st Ability",
+ "SurvivorSecondAbility": "Number of players left to get 2nd Ability",
+ "SurvivorThirdAbility": "Number of players left to get 3rd Ability",
+ "SurvivorLastAbility": "Number of players left to get Last Ability",
+ "SurvivorCurrentAlive": "- Alive Players:",
+ "SurvivorCantKillYet": "The current number of players is greater than your killing requirement, which means you cannot kill yet.",
"Swapper": "Swapper",
"SwapperInfo": "Swap players' votes",
"SwapperInfoLong": "(Crewmates):\\nAs the Swapper, you can use the command /cmd sw [ID] twice to choose 2 players. Those votes received by those 2 players will be swapped.\\nYou can see everyone's IDs next to their name.\\n\\nFor example, if 3 players voted red, and 5 players voted yellow, and you swap red and yellows' IDs, then red will receive 5 votes, and yellow will receive 3 votes.\\n\\nDue to technical difficulties, the Host's original vote will seem unchanged, but it's only visual, their vote will still get swapped.",
diff --git a/Roles/Standard/Crewmate/Power/Survivor.cs b/Roles/Standard/Crewmate/Power/Survivor.cs
new file mode 100644
index 000000000..7a96682c9
--- /dev/null
+++ b/Roles/Standard/Crewmate/Power/Survivor.cs
@@ -0,0 +1,197 @@
+using System;
+using System.Linq;
+using AmongUs.GameOptions;
+using EHR.Modules;
+using EHR.Modules.Extensions;
+using Hazel;
+using UnityEngine;
+
+namespace EHR.Roles;
+
+using static Options;
+using static Translator;
+using static Utils;
+
+public class Survivor : RoleBase
+{
+ private const int Id = 7680;
+ public static bool On;
+
+ public static OptionItem FirstAbility;
+ public static OptionItem SecondAbility;
+ public static OptionItem ThirdAbility;
+ public static OptionItem LastAbility;
+ public static OptionItem ShieldCooldown;
+ public static OptionItem ShieldDuration;
+ public static OptionItem AdditionalVote;
+ public static OptionItem KillCooldown;
+
+ private CountdownTimer ShieldTimer;
+ private byte SurvivorId;
+ private bool Killing;
+ public override bool IsEnable => On;
+
+ public override void SetupCustomOption()
+ {
+ SetupRoleOptions(Id, TabGroup.CrewmateRoles, CustomRoles.Survivor);
+
+ FirstAbility = new IntegerOptionItem(Id + 11, "SurvivorFirstAbility", new(0, 70, 1), 10, TabGroup.CrewmateRoles).SetParent(CustomRoleSpawnChances[CustomRoles.Survivor]);
+
+ SecondAbility = new IntegerOptionItem(Id + 12, "SurvivorSecondAbility", new(0, 70, 1), 8, TabGroup.CrewmateRoles).SetParent(CustomRoleSpawnChances[CustomRoles.Survivor]);
+
+ ThirdAbility = new IntegerOptionItem(Id + 13, "SurvivorThirdAbility", new(0, 70, 1), 6, TabGroup.CrewmateRoles).SetParent(CustomRoleSpawnChances[CustomRoles.Survivor]);
+
+ LastAbility = new IntegerOptionItem(Id + 14, "SurvivorLastAbility", new(0, 70, 1), 4, TabGroup.CrewmateRoles).SetParent(CustomRoleSpawnChances[CustomRoles.Survivor]);
+
+ ShieldCooldown = new FloatOptionItem(Id + 15, "AidCD", new(0f, 70f, 1f), 15f, TabGroup.CrewmateRoles).SetParent(CustomRoleSpawnChances[CustomRoles.Survivor])
+ .SetValueFormat(OptionFormat.Seconds);
+
+ ShieldDuration = new FloatOptionItem(Id + 16, "AidDur", new(5f, 70f, 1f), 20f, TabGroup.CrewmateRoles).SetParent(CustomRoleSpawnChances[CustomRoles.Survivor])
+ .SetValueFormat(OptionFormat.Seconds);
+
+ AdditionalVote = new IntegerOptionItem(Id + 17, "MayorAdditionalVote", new(0, 90, 1), 2, TabGroup.CrewmateRoles).SetParent(CustomRoleSpawnChances[CustomRoles.Survivor]);
+
+ KillCooldown = new FloatOptionItem(Id + 18, "KillCooldown", new(5f, 70f, 1f), 20f, TabGroup.CrewmateRoles).SetParent(CustomRoleSpawnChances[CustomRoles.Survivor])
+ .SetValueFormat(OptionFormat.Seconds);
+ }
+
+ public override void Init()
+ {
+ On = false;
+ }
+
+ public override void Add(byte playerId)
+ {
+ On = true;
+ ShieldTimer = null;
+ Killing = false;
+ SurvivorId = playerId;
+ }
+
+ public override void ApplyGameOptions(IGameOptions opt, byte playerId)
+ {
+ AURoleOptions.PhantomCooldown = ShieldCooldown.GetFloat();
+ AURoleOptions.EngineerCooldown = ShieldCooldown.GetFloat();
+ AURoleOptions.EngineerInVentMaxTime = 1f;
+ }
+
+ public void ReceiveRPC(MessageReader reader)
+ {
+ Killing = reader.ReadBoolean();
+ }
+
+ // First ability: Players alive count
+ public override void GetProgressText(byte playerId, bool comms, StringBuilder resultText)
+ {
+ int CPA = Main.AllAlivePlayerControlsCount;
+
+ resultText.Append(GetTaskCount(playerId, comms));
+
+ if (CPA <= FirstAbility.GetInt())
+ resultText.Append(' ').Append(ColorPrefix(Color.yellow))
+ .Append(GetString("SurvivorCurrentAlive"))
+ .Append(' ').Append(CPA)
+ .Append("");
+ }
+
+ // Second ability: Shield
+ public override void AfterMeetingTasks()
+ {
+ ShieldTimer?.Dispose();
+ ShieldTimer = null;
+ }
+
+ public override void SetButtonTexts(HudManager hud, byte id)
+ {
+ hud.AbilityButton?.OverrideText(GetString("AbilityButtonText.GuardianAngel"));
+ }
+
+ public override bool OnVanish(PlayerControl pc)
+ {
+ ShieldSelf(pc);
+ return false;
+ }
+
+ public override void OnEnterVent(PlayerControl pc, Vent vent)
+ {
+ if (!Killing)
+ ShieldSelf(pc);
+ }
+
+ private void ShieldSelf(PlayerControl pc)
+ {
+ ShieldTimer?.Dispose();
+ if (Main.AllAlivePlayerControlsCount <= SecondAbility.GetInt())
+ {
+ bool shielded = ShieldTimer != null;
+ ShieldTimer = new CountdownTimer(ShieldDuration.GetFloat(), () =>
+ {
+ ShieldTimer = null;
+ Utils.NotifyRoles(SpecifySeer: pc, SpecifyTarget: pc);
+ }, onCanceled: () => ShieldTimer = null);
+ if (!shielded) Utils.NotifyRoles(SpecifySeer: pc, SpecifyTarget: pc);
+ }
+ }
+
+ public override bool OnCheckMurderAsTarget(PlayerControl killer, PlayerControl target)
+ {
+ return ShieldTimer == null;
+ }
+
+ public override string GetSuffix(PlayerControl seer, PlayerControl target, bool hud = false, bool meeting = false)
+ {
+ if (seer.PlayerId != target.PlayerId || seer.PlayerId != SurvivorId || meeting || (seer.IsModdedClient() && !hud) || ShieldTimer == null) return string.Empty;
+ return seer.IsHost() ? string.Format(Translator.GetString("SafeguardSuffixTimer"), (int)Math.Ceiling(ShieldTimer.Remaining.TotalSeconds)) : Translator.GetString("SafeguardSuffix");
+ }
+
+ public override bool CanUseVent(PlayerControl pc, int ventId)
+ {
+ return !IsThisRole(pc) || pc.GetClosestVent()?.Id == ventId || pc.Is(CustomRoles.Nimble);
+ }
+
+ public override bool CanUseImpostorVentButton(PlayerControl pc)
+ {
+ return pc.Is(CustomRoles.Nimble);
+ }
+
+ // Third ability: Votes
+ public override void ManipulateGameEndCheckCrew(PlayerState playerState, out bool keepGameGoing, out int countsAs)
+ {
+ if (playerState.IsDead)
+ {
+ base.ManipulateGameEndCheckCrew(playerState, out keepGameGoing, out countsAs);
+ return;
+ }
+
+ keepGameGoing = true;
+ countsAs = 1;
+ }
+
+ // Last ability: Kill
+ public override void OnTaskComplete(PlayerControl pc, int completedTaskCount, int totalTaskCount)
+ {
+ if (completedTaskCount + 1 >= totalTaskCount)
+ {
+ Killing = true;
+ pc.RpcChangeRoleBasis(CustomRoles.Scanner);
+ LateTask.New(() => pc.SetKillCooldown(KillCooldown.GetFloat()), 0.2f);
+ Utils.SendRPC(CustomRPC.SyncRoleData, pc.PlayerId, Killing);
+ Utils.NotifyRoles(SpecifySeer: pc, SpecifyTarget: pc);
+ pc.MarkDirtySettings();
+ }
+ }
+
+ public override bool CanUseKillButton(PlayerControl pc) { return Killing; }
+
+ public override void SetKillCooldown(byte id)
+ {
+ Main.AllPlayerKillCooldown[id] = KillCooldown.GetFloat();
+ }
+
+ public override bool OnCheckMurder(PlayerControl killer, PlayerControl target)
+ {
+ if (Main.AllAlivePlayerControlsCount <= LastAbility.GetInt()) return true;
+ killer.Notify(Translator.GetString("SurvivorCantKillYet"));
+ return false;
+ }
+}
diff --git a/Roles/Standard/Neutral/Killing/Accumulator.cs b/Roles/Standard/Neutral/Killing/Accumulator.cs
index 7a9c1afd8..b5ac0c09b 100644
--- a/Roles/Standard/Neutral/Killing/Accumulator.cs
+++ b/Roles/Standard/Neutral/Killing/Accumulator.cs
@@ -107,4 +107,4 @@ public class Accumulator : RoleBase
if (seer.PlayerId != AccumulatorId || seer.PlayerId != target.PlayerId || (seer.IsModdedClient() && !hud) || meeting || Killing) return string.Empty;
return string.Format(Translator.GetString("KCD"), Math.Round(Math.Max(0, KCD), 1));
}
-}
\ No newline at end of file
+}