Merge remote-tracking branch 'origin/main'

This commit is contained in:
Gurge44 committed 2026-05-19 10:25:54 +02:00
commit a81a1b5089
10 files changed
+228 -8

No files matched your search

+1
View File
@@ -273,6 +273,7 @@ public enum CustomRoles
SpeedBooster,
Spy,
SuperStar,
Survivor,
Swapper,
Tar,
TaskManager,
+1
View File
@@ -273,6 +273,7 @@ public enum CustomRoles
SpeedBooster,
Spy,
SuperStar,
Survivor,
Swapper,
Tar,
TaskManager,
+1
View File
@@ -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" },
+7 -4
View File
@@ -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,
+2 -2
View File
@@ -366,7 +366,7 @@ internal static class GameEndChecker
}
}
toAdd.ForEach(x => WinnerIds.Add(x));
toAdd?.ForEach(x => WinnerIds.Add(x));
foreach (CustomRoles role in WinnerRoles) WinnerIds.UnionWith(Main.EnumeratePlayerControls().Where(x => x.GetCustomRole() == role).Select(x => x.PlayerId));
}
@@ -1517,4 +1517,4 @@ internal static class CheckEndGameViaTasksPatch
__result = false;
return false;
}
}
}
+5
View File
@@ -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:
{
+3
View File
@@ -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;
+10 -1
View File
@@ -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": "<color=#ffff00>The Spiritualist has an arrow pointing toward you</color>",
@@ -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 <b>{0}</b> 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 <b>/cmd sw <color=#0000ff>[ID]</color></b> twice to choose 2 players. Those votes received by those 2 players will be swapped.\\nYou can see everyone's <color=#0000ff>ID</color>s 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.",
+197
View File
@@ -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("</color>");
}
// 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;
}
}
@@ -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));
}
}
}