From f50e95857669f585c271833733bbdc3c0ccade64 Mon Sep 17 00:00:00 2001 From: Newchan Date: Mon, 18 May 2026 21:31:09 +0700 Subject: [PATCH] Add Survivor (#602) * Add Survivor role and abilities to en_US.jsonc * Update en_US.jsonc * Create Survivor.cs * Update Accumulator.cs * Update MeetingHudPatch.cs * Update Utils.cs * Update CustomRoles.cs * Update CustomRoles.cs * Update CustomRolesHelper.cs * Update HudSpritePatch.cs * Update Main.cs * Give up on pet, just vent/vanish to shield * Update HudSpritePatch.cs * Update Utils.cs * Update CustomRolesHelper.cs * Update en_US.jsonc * Update Survivor.cs * I think gurge is very mad now * revert things cuz I dont think thats a fix * okay * better idea: changes basis on task completion, then kill attempt checks the player count instead * Update Survivor.cs * done, now consider this pls * the answer is yes * I think void ChangeBasis thingy is no longer needed * since scanner is a crew role uses phantom basis, its better to use rather than phantom role itself * survivor cant kill notification --- CTA/CustomTeamAssigner/CustomRoles.cs | 1 + CustomRoles.cs | 1 + Main.cs | 1 + Modules/CustomRolesHelper.cs | 11 +- Patches/HudSpritePatch.cs | 5 + Patches/MeetingHudPatch.cs | 3 + Resources/Lang/en_US.jsonc | 11 +- Roles/Standard/Crewmate/Power/Survivor.cs | 197 ++++++++++++++++++ Roles/Standard/Neutral/Killing/Accumulator.cs | 2 +- 9 files changed, 226 insertions(+), 6 deletions(-) create mode 100644 Roles/Standard/Crewmate/Power/Survivor.cs 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 +}